@juspay/neurolink 10.5.3 → 10.6.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/agent/agent.js +39 -8
- package/dist/agent/agentNetwork.js +62 -20
- package/dist/agent/agentToolRegistrar.d.ts +48 -0
- package/dist/agent/agentToolRegistrar.js +336 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.js +4 -0
- package/dist/agent/isolatedAgentRunner.d.ts +73 -0
- package/dist/agent/isolatedAgentRunner.js +946 -0
- package/dist/agent/structuredRecovery.d.ts +30 -0
- package/dist/agent/structuredRecovery.js +104 -0
- package/dist/browser/neurolink.min.js +414 -398
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/core/baseProvider.d.ts +8 -0
- package/dist/core/baseProvider.js +40 -19
- package/dist/core/modules/GenerationHandler.d.ts +2 -6
- package/dist/core/modules/GenerationHandler.js +10 -1
- package/dist/core/toolExecutionRecorder.d.ts +76 -0
- package/dist/core/toolExecutionRecorder.js +261 -0
- package/dist/evaluation/contextBuilder.js +6 -6
- package/dist/lib/agent/agent.js +39 -8
- package/dist/lib/agent/agentNetwork.js +62 -20
- package/dist/lib/agent/agentToolRegistrar.d.ts +48 -0
- package/dist/lib/agent/agentToolRegistrar.js +337 -0
- package/dist/lib/agent/index.d.ts +3 -0
- package/dist/lib/agent/index.js +4 -0
- package/dist/lib/agent/isolatedAgentRunner.d.ts +73 -0
- package/dist/lib/agent/isolatedAgentRunner.js +947 -0
- package/dist/lib/agent/structuredRecovery.d.ts +30 -0
- package/dist/lib/agent/structuredRecovery.js +105 -0
- package/dist/lib/core/baseProvider.d.ts +8 -0
- package/dist/lib/core/baseProvider.js +40 -19
- package/dist/lib/core/modules/GenerationHandler.d.ts +2 -6
- package/dist/lib/core/modules/GenerationHandler.js +10 -1
- package/dist/lib/core/toolExecutionRecorder.d.ts +76 -0
- package/dist/lib/core/toolExecutionRecorder.js +262 -0
- package/dist/lib/evaluation/contextBuilder.js +6 -6
- package/dist/lib/models/modelRegistry.d.ts +23 -0
- package/dist/lib/models/modelRegistry.js +62 -1
- package/dist/lib/models/modelResolver.js +1 -0
- package/dist/lib/neurolink.d.ts +80 -1
- package/dist/lib/neurolink.js +199 -37
- package/dist/lib/providers/amazonBedrock.js +15 -2
- package/dist/lib/providers/anthropic.js +23 -11
- package/dist/lib/providers/googleAiStudio.js +8 -1
- package/dist/lib/providers/googleNativeGemini3.d.ts +2 -0
- package/dist/lib/providers/googleNativeGemini3.js +8 -1
- package/dist/lib/providers/googleVertex.js +56 -16
- package/dist/lib/providers/openaiChatCompletionsClient.js +18 -5
- package/dist/lib/tasks/autoresearchTaskExecutor.js +7 -6
- package/dist/lib/tasks/taskExecutor.js +2 -5
- package/dist/lib/types/agentNetwork.d.ts +23 -8
- package/dist/lib/types/generate.d.ts +62 -5
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/isolatedAgent.d.ts +365 -0
- package/dist/lib/types/isolatedAgent.js +12 -0
- package/dist/lib/types/model.d.ts +8 -0
- package/dist/lib/types/stream.d.ts +3 -1
- package/dist/lib/types/task.d.ts +3 -3
- package/dist/lib/utils/logger.d.ts +12 -3
- package/dist/lib/utils/logger.js +11 -3
- package/dist/lib/utils/transformationUtils.d.ts +14 -0
- package/dist/lib/utils/transformationUtils.js +33 -10
- package/dist/lib/voice/livekit/realtimeVoiceAgent.js +6 -3
- package/dist/models/modelRegistry.d.ts +23 -0
- package/dist/models/modelRegistry.js +62 -1
- package/dist/models/modelResolver.js +1 -0
- package/dist/neurolink.d.ts +80 -1
- package/dist/neurolink.js +199 -37
- package/dist/providers/amazonBedrock.js +15 -2
- package/dist/providers/anthropic.js +23 -11
- package/dist/providers/googleAiStudio.js +8 -1
- package/dist/providers/googleNativeGemini3.d.ts +2 -0
- package/dist/providers/googleNativeGemini3.js +8 -1
- package/dist/providers/googleVertex.js +56 -16
- package/dist/providers/openaiChatCompletionsClient.js +18 -5
- package/dist/tasks/autoresearchTaskExecutor.js +7 -6
- package/dist/tasks/taskExecutor.js +2 -5
- package/dist/types/agentNetwork.d.ts +23 -8
- package/dist/types/generate.d.ts +62 -5
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/isolatedAgent.d.ts +365 -0
- package/dist/types/isolatedAgent.js +11 -0
- package/dist/types/model.d.ts +8 -0
- package/dist/types/stream.d.ts +3 -1
- package/dist/types/task.d.ts +3 -3
- package/dist/utils/logger.d.ts +12 -3
- package/dist/utils/logger.js +11 -3
- package/dist/utils/transformationUtils.d.ts +14 -0
- package/dist/utils/transformationUtils.js +33 -10
- package/dist/voice/livekit/realtimeVoiceAgent.js +6 -3
- package/package.json +3 -2
|
@@ -13,6 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
import { logger } from "../utils/logger.js";
|
|
14
14
|
import { ErrorFactory } from "../utils/errorHandling.js";
|
|
15
15
|
import { Agent } from "./agent.js";
|
|
16
|
+
import { acquireDelegationSlot, runWithNestedDelegationDepth, } from "./agentToolRegistrar.js";
|
|
16
17
|
/**
|
|
17
18
|
* AgentNetwork - Multi-agent orchestration using the ai SDK tool loop
|
|
18
19
|
*
|
|
@@ -171,21 +172,62 @@ export class AgentNetwork {
|
|
|
171
172
|
// Capture in closure so the async execute below closes over the right values
|
|
172
173
|
const capturedId = id;
|
|
173
174
|
const capturedAgent = agentInstance;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
// Honor the agent's declared inputSchema; default to a plain task
|
|
176
|
+
// string when the definition doesn't declare one.
|
|
177
|
+
const customSchema = capturedAgent.inputSchema;
|
|
178
|
+
const schema = customSchema ??
|
|
179
|
+
z.object({
|
|
180
|
+
task: z.string().describe("The task to delegate to this agent"),
|
|
181
|
+
});
|
|
177
182
|
tools[`agent_${capturedId}`] = tool({
|
|
178
183
|
description: `Agent: ${capturedAgent.name} - ${capturedAgent.description}`,
|
|
179
184
|
inputSchema: schema,
|
|
180
185
|
execute: async (params) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
// With a custom inputSchema the parsed object passes through to
|
|
187
|
+
// the agent untouched; the default schema unwraps the task string.
|
|
188
|
+
const input = customSchema
|
|
189
|
+
? params
|
|
190
|
+
: params.task;
|
|
191
|
+
logger.debug(`[AgentNetwork:${this.id}] Delegating to agent: ${capturedAgent.name}`, {
|
|
192
|
+
task: typeof input === "string"
|
|
193
|
+
? input.slice(0, 100)
|
|
194
|
+
: input && typeof input === "object"
|
|
195
|
+
? Object.keys(input).join(",")
|
|
196
|
+
: String(input),
|
|
197
|
+
});
|
|
198
|
+
// Standalone-mode delegations share the same process-wide pool as
|
|
199
|
+
// host-loop delegations (registerAgentTool), so concurrent agent
|
|
200
|
+
// fan-out is bounded framework-wide. Refusals carry the recovery
|
|
201
|
+
// instruction, mirroring the host-loop contract.
|
|
202
|
+
let release;
|
|
203
|
+
try {
|
|
204
|
+
release = await acquireDelegationSlot();
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return {
|
|
208
|
+
agentId: capturedId,
|
|
209
|
+
content: "",
|
|
210
|
+
status: "error",
|
|
211
|
+
error: `All delegation slots are busy and the queue timed out. Do not retry agent_${capturedId} immediately; synthesize from the results you already have.`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
// The agent executes one delegation level deeper: this tool call
|
|
216
|
+
// holds a pool slot for its whole duration, so any
|
|
217
|
+
// registered-agent-tool delegation the agent makes from inside
|
|
218
|
+
// must take the nested path instead of queueing behind the slot
|
|
219
|
+
// its own caller is holding.
|
|
220
|
+
const result = await runWithNestedDelegationDepth(() => capturedAgent.execute(input));
|
|
221
|
+
return {
|
|
222
|
+
agentId: capturedId,
|
|
223
|
+
content: result.content,
|
|
224
|
+
status: result.status,
|
|
225
|
+
error: result.error,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
release();
|
|
230
|
+
}
|
|
189
231
|
},
|
|
190
232
|
});
|
|
191
233
|
}
|
|
@@ -214,9 +256,9 @@ Use the appropriate agent tool(s) to handle the task. Return a clear, complete f
|
|
|
214
256
|
buildTrace(toolExecutions, traceId, startTime) {
|
|
215
257
|
const steps = (toolExecutions ?? []).map((exec, index) => {
|
|
216
258
|
// Tool name format is "agent_<id>" — extract agent id
|
|
217
|
-
const agentId = exec.
|
|
218
|
-
? exec.
|
|
219
|
-
: exec.
|
|
259
|
+
const agentId = exec.toolName.startsWith("agent_")
|
|
260
|
+
? exec.toolName.slice("agent_".length)
|
|
261
|
+
: exec.toolName;
|
|
220
262
|
const primitive = this.primitives.get(agentId);
|
|
221
263
|
return {
|
|
222
264
|
index,
|
|
@@ -225,10 +267,11 @@ Use the appropriate agent tool(s) to handle the task. Return a clear, complete f
|
|
|
225
267
|
id: agentId,
|
|
226
268
|
name: primitive?.name ?? agentId,
|
|
227
269
|
},
|
|
228
|
-
input: exec.
|
|
229
|
-
output: exec.
|
|
230
|
-
|
|
231
|
-
|
|
270
|
+
input: exec.params,
|
|
271
|
+
output: exec.resultText,
|
|
272
|
+
...(exec.isError && { error: exec.resultText }),
|
|
273
|
+
duration: exec.durationMs,
|
|
274
|
+
timestamp: exec.startedAt || startTime,
|
|
232
275
|
};
|
|
233
276
|
});
|
|
234
277
|
return {
|
|
@@ -272,8 +315,7 @@ Use the appropriate agent tool(s) to handle the task. Return a clear, complete f
|
|
|
272
315
|
maxSteps,
|
|
273
316
|
tools: agentTools,
|
|
274
317
|
});
|
|
275
|
-
const
|
|
276
|
-
const trace = this.buildTrace(toolExecutions, traceId, startTime);
|
|
318
|
+
const trace = this.buildTrace(result.toolExecutions, traceId, startTime);
|
|
277
319
|
// Aggregate token usage across all agent tool calls
|
|
278
320
|
const usage = result.usage;
|
|
279
321
|
const totalUsage = {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-loop delegation (N5): `registerAgentTool()` wraps an isolated agent
|
|
3
|
+
* (runIsolatedAgent) as a tool on the HOST NeuroLink instance, so the host's
|
|
4
|
+
* EXISTING generate() loop delegates — never a second router generate.
|
|
5
|
+
*
|
|
6
|
+
* Framework policy lives here:
|
|
7
|
+
* - per-turn delegation caps, counted per top-level generate() in the loop
|
|
8
|
+
* itself (AsyncLocalStorage turn scope);
|
|
9
|
+
* - depth limits via tool context (`agentDepth`) — at the limit the tool is
|
|
10
|
+
* withheld from the request entirely;
|
|
11
|
+
* - a process-wide concurrency pool with queue timeout, shared by every
|
|
12
|
+
* registered agent tool;
|
|
13
|
+
* - every refusal carries its recovery instruction in the error text.
|
|
14
|
+
*/
|
|
15
|
+
import type { NeuroLink } from "../neurolink.js";
|
|
16
|
+
import type { AgentToolRegistrationOptions, GenerateOptions, IsolatedAgentDefinition } from "../types/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Enter a per-turn delegation scope for a top-level generate(). Returns null
|
|
19
|
+
* when a scope is already active (nested/internal generates share the
|
|
20
|
+
* top-level turn's counters) — the caller then proceeds without wrapping.
|
|
21
|
+
*/
|
|
22
|
+
export declare function beginDelegationTurn(host: NeuroLink, options: GenerateOptions | string | Record<string, unknown>): {
|
|
23
|
+
options: typeof options;
|
|
24
|
+
run: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* Run `fn` one delegation level deeper, sharing the current turn's counters.
|
|
28
|
+
* Used by AgentNetwork's standalone delegations: the network holds a pool
|
|
29
|
+
* slot around each agent execution, so any registered-agent-tool delegation
|
|
30
|
+
* the agent makes from inside must take the NESTED path (pool bypass, depth
|
|
31
|
+
* checks) — request-scoped via AsyncLocalStorage, never instance state.
|
|
32
|
+
*/
|
|
33
|
+
export declare function runWithNestedDelegationDepth<T>(fn: () => Promise<T>): Promise<T>;
|
|
34
|
+
/**
|
|
35
|
+
* Acquire a slot in the process-wide delegation pool, waiting up to
|
|
36
|
+
* `timeoutMs` in the queue. Resolves to a release function.
|
|
37
|
+
*/
|
|
38
|
+
export declare function acquireDelegationSlot(timeoutMs?: number): Promise<() => void>;
|
|
39
|
+
/**
|
|
40
|
+
* Register an isolated agent as a delegation tool on the host instance.
|
|
41
|
+
* The tool becomes available to every generate()/stream() the host runs —
|
|
42
|
+
* inside its existing loop, no second router generate.
|
|
43
|
+
*/
|
|
44
|
+
export declare function registerAgentTool(host: NeuroLink, definition: IsolatedAgentDefinition, options?: AgentToolRegistrationOptions): {
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
47
|
+
/** Test-only: reset the process-wide pool. */
|
|
48
|
+
export declare function resetDelegationPoolForTests(capacity?: number): void;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-loop delegation (N5): `registerAgentTool()` wraps an isolated agent
|
|
3
|
+
* (runIsolatedAgent) as a tool on the HOST NeuroLink instance, so the host's
|
|
4
|
+
* EXISTING generate() loop delegates — never a second router generate.
|
|
5
|
+
*
|
|
6
|
+
* Framework policy lives here:
|
|
7
|
+
* - per-turn delegation caps, counted per top-level generate() in the loop
|
|
8
|
+
* itself (AsyncLocalStorage turn scope);
|
|
9
|
+
* - depth limits via tool context (`agentDepth`) — at the limit the tool is
|
|
10
|
+
* withheld from the request entirely;
|
|
11
|
+
* - a process-wide concurrency pool with queue timeout, shared by every
|
|
12
|
+
* registered agent tool;
|
|
13
|
+
* - every refusal carries its recovery instruction in the error text.
|
|
14
|
+
*/
|
|
15
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
16
|
+
import { logger } from "../utils/logger.js";
|
|
17
|
+
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
18
|
+
import { hasOpenIsolatedAgentHandle, runIsolatedAgent, } from "./isolatedAgentRunner.js";
|
|
19
|
+
const DEFAULT_POOL_CAPACITY = 4;
|
|
20
|
+
const DEFAULT_POOL_QUEUE_TIMEOUT_MS = 30_000;
|
|
21
|
+
/** Bound on the content text returned into the host loop per delegation. */
|
|
22
|
+
const DELEGATION_RESULT_CONTENT_CHARS = 4000;
|
|
23
|
+
// ── Registrations ──────────────────────────────────────────────────────────
|
|
24
|
+
const hostRegistrations = new WeakMap();
|
|
25
|
+
function registrationsFor(host) {
|
|
26
|
+
let map = hostRegistrations.get(host);
|
|
27
|
+
if (!map) {
|
|
28
|
+
map = new Map();
|
|
29
|
+
hostRegistrations.set(host, map);
|
|
30
|
+
}
|
|
31
|
+
return map;
|
|
32
|
+
}
|
|
33
|
+
// ── Per-turn scope (delegation caps) ───────────────────────────────────────
|
|
34
|
+
const turnStorage = new AsyncLocalStorage();
|
|
35
|
+
/**
|
|
36
|
+
* Enter a per-turn delegation scope for a top-level generate(). Returns null
|
|
37
|
+
* when a scope is already active (nested/internal generates share the
|
|
38
|
+
* top-level turn's counters) — the caller then proceeds without wrapping.
|
|
39
|
+
*/
|
|
40
|
+
export function beginDelegationTurn(host, options) {
|
|
41
|
+
if (turnStorage.getStore()) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const registrations = registrationsFor(host);
|
|
45
|
+
let scopedOptions = options;
|
|
46
|
+
if (registrations.size > 0 && typeof options !== "string") {
|
|
47
|
+
const depth = resolveDepth(host, options);
|
|
48
|
+
const withheld = [...registrations.values()]
|
|
49
|
+
.filter((r) => r.options.maxDepth !== undefined && depth >= r.options.maxDepth)
|
|
50
|
+
.map((r) => r.name);
|
|
51
|
+
if (withheld.length > 0) {
|
|
52
|
+
const generateOptions = options;
|
|
53
|
+
scopedOptions = {
|
|
54
|
+
...generateOptions,
|
|
55
|
+
excludeTools: [...(generateOptions.excludeTools ?? []), ...withheld],
|
|
56
|
+
};
|
|
57
|
+
logger.debug("[AgentToolRegistrar] Withholding depth-limited agent tools", { withheld, depth });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const state = {
|
|
61
|
+
counts: new Map(),
|
|
62
|
+
depth: resolveDepth(host, typeof options === "string" ? undefined : options),
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
options: scopedOptions,
|
|
66
|
+
run: (fn) => turnStorage.run(state, fn),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Run `fn` one delegation level deeper, sharing the current turn's counters.
|
|
71
|
+
* Used by AgentNetwork's standalone delegations: the network holds a pool
|
|
72
|
+
* slot around each agent execution, so any registered-agent-tool delegation
|
|
73
|
+
* the agent makes from inside must take the NESTED path (pool bypass, depth
|
|
74
|
+
* checks) — request-scoped via AsyncLocalStorage, never instance state.
|
|
75
|
+
*/
|
|
76
|
+
export function runWithNestedDelegationDepth(fn) {
|
|
77
|
+
const store = turnStorage.getStore();
|
|
78
|
+
const state = store
|
|
79
|
+
? { counts: store.counts, depth: store.depth + 1 }
|
|
80
|
+
: { counts: new Map(), depth: 1 };
|
|
81
|
+
return turnStorage.run(state, fn);
|
|
82
|
+
}
|
|
83
|
+
function resolveDepth(host, options, executionContext) {
|
|
84
|
+
// Execution context wins: it carries the agentDepth stamped on the worker
|
|
85
|
+
// that is actually making this call (see executeDelegation).
|
|
86
|
+
const fromExecution = executionContext?.agentDepth;
|
|
87
|
+
if (typeof fromExecution === "number") {
|
|
88
|
+
return fromExecution;
|
|
89
|
+
}
|
|
90
|
+
const fromCall = options?.context?.agentDepth;
|
|
91
|
+
if (typeof fromCall === "number") {
|
|
92
|
+
return fromCall;
|
|
93
|
+
}
|
|
94
|
+
const fromInstance = host.getToolContext()?.agentDepth;
|
|
95
|
+
return typeof fromInstance === "number" ? fromInstance : 0;
|
|
96
|
+
}
|
|
97
|
+
// ── Process-wide delegation pool ───────────────────────────────────────────
|
|
98
|
+
let poolCapacity = DEFAULT_POOL_CAPACITY;
|
|
99
|
+
let poolInUse = 0;
|
|
100
|
+
const poolWaiters = [];
|
|
101
|
+
function releasePoolSlot() {
|
|
102
|
+
poolInUse = Math.max(0, poolInUse - 1);
|
|
103
|
+
const next = poolWaiters.shift();
|
|
104
|
+
if (next) {
|
|
105
|
+
poolInUse++;
|
|
106
|
+
next.grant();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Acquire a slot in the process-wide delegation pool, waiting up to
|
|
111
|
+
* `timeoutMs` in the queue. Resolves to a release function.
|
|
112
|
+
*/
|
|
113
|
+
export function acquireDelegationSlot(timeoutMs = DEFAULT_POOL_QUEUE_TIMEOUT_MS) {
|
|
114
|
+
if (poolInUse < poolCapacity) {
|
|
115
|
+
poolInUse++;
|
|
116
|
+
let released = false;
|
|
117
|
+
return Promise.resolve(() => {
|
|
118
|
+
if (!released) {
|
|
119
|
+
released = true;
|
|
120
|
+
releasePoolSlot();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const waiter = {
|
|
126
|
+
grant: () => {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
let released = false;
|
|
129
|
+
resolve(() => {
|
|
130
|
+
if (!released) {
|
|
131
|
+
released = true;
|
|
132
|
+
releasePoolSlot();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
cancel: (reason) => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
reject(reason);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const timer = setTimeout(() => {
|
|
142
|
+
const index = poolWaiters.indexOf(waiter);
|
|
143
|
+
if (index !== -1) {
|
|
144
|
+
poolWaiters.splice(index, 1);
|
|
145
|
+
}
|
|
146
|
+
waiter.cancel(new Error("delegation pool queue timeout"));
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
timer.unref?.();
|
|
149
|
+
poolWaiters.push(waiter);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// ── Refusals (recovery instruction always included) ────────────────────────
|
|
153
|
+
function refusal(message) {
|
|
154
|
+
return { isError: true, error: message };
|
|
155
|
+
}
|
|
156
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
157
|
+
/**
|
|
158
|
+
* Register an isolated agent as a delegation tool on the host instance.
|
|
159
|
+
* The tool becomes available to every generate()/stream() the host runs —
|
|
160
|
+
* inside its existing loop, no second router generate.
|
|
161
|
+
*/
|
|
162
|
+
export function registerAgentTool(host, definition, options = {}) {
|
|
163
|
+
if (!definition?.id || !definition?.instructions) {
|
|
164
|
+
throw new Error("registerAgentTool: definition must include id and instructions");
|
|
165
|
+
}
|
|
166
|
+
const name = options.name ?? definition.id;
|
|
167
|
+
const registrations = registrationsFor(host);
|
|
168
|
+
if (registrations.has(name)) {
|
|
169
|
+
throw new Error(`registerAgentTool: an agent tool named "${name}" is already registered on this instance`);
|
|
170
|
+
}
|
|
171
|
+
if (options.maxConcurrent !== undefined) {
|
|
172
|
+
// Process-wide pool: the largest registered capacity wins (raises only —
|
|
173
|
+
// never lowers; this is not a per-agent throttle). Grant any queued
|
|
174
|
+
// waiters the raise just unblocked.
|
|
175
|
+
poolCapacity = Math.max(poolCapacity, options.maxConcurrent);
|
|
176
|
+
while (poolInUse < poolCapacity && poolWaiters.length > 0) {
|
|
177
|
+
const next = poolWaiters.shift();
|
|
178
|
+
if (next) {
|
|
179
|
+
poolInUse++;
|
|
180
|
+
next.grant();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const registration = {
|
|
185
|
+
name,
|
|
186
|
+
definition,
|
|
187
|
+
options,
|
|
188
|
+
};
|
|
189
|
+
registrations.set(name, registration);
|
|
190
|
+
const inputSchema = definition.inputSchema
|
|
191
|
+
? convertZodToJsonSchema(definition.inputSchema)
|
|
192
|
+
: {
|
|
193
|
+
type: "object",
|
|
194
|
+
properties: {
|
|
195
|
+
task: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "The task to delegate to this agent",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: ["task"],
|
|
201
|
+
};
|
|
202
|
+
host.registerTool(name, {
|
|
203
|
+
name,
|
|
204
|
+
description: `Delegate to agent: ${definition.name} — ${definition.description}`,
|
|
205
|
+
inputSchema: inputSchema,
|
|
206
|
+
execute: async (params, context) => {
|
|
207
|
+
return executeDelegation(host, registration, params, context);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
logger.debug("[AgentToolRegistrar] Registered agent tool", {
|
|
211
|
+
name,
|
|
212
|
+
agentId: definition.id,
|
|
213
|
+
maxDelegationsPerTurn: options.maxDelegationsPerTurn,
|
|
214
|
+
maxDepth: options.maxDepth,
|
|
215
|
+
});
|
|
216
|
+
return { name };
|
|
217
|
+
}
|
|
218
|
+
async function executeDelegation(host, registration, params, context) {
|
|
219
|
+
const { name, definition, options } = registration;
|
|
220
|
+
const contextRecord = context && typeof context === "object"
|
|
221
|
+
? context
|
|
222
|
+
: {};
|
|
223
|
+
// Depth comes from the EXECUTION context first: when a worker created with
|
|
224
|
+
// the shared tool registry delegates, the registration lives on the host
|
|
225
|
+
// but the call arrives with the worker's tool context carrying the
|
|
226
|
+
// agentDepth this registrar set — the host instance context would read 0
|
|
227
|
+
// every time and nested delegation would never hit the limit. When the
|
|
228
|
+
// execution context carries no depth, the ALS turn scope's depth covers
|
|
229
|
+
// the composed path (AgentNetwork wraps its agents one level deeper).
|
|
230
|
+
const turnScope = turnStorage.getStore();
|
|
231
|
+
const depth = typeof contextRecord.agentDepth === "number"
|
|
232
|
+
? contextRecord.agentDepth
|
|
233
|
+
: (turnScope?.depth ?? resolveDepth(host));
|
|
234
|
+
if (options.maxDepth !== undefined && depth >= options.maxDepth) {
|
|
235
|
+
return refusal(`Delegation depth limit reached (${depth}/${options.maxDepth}). Complete this investigation yourself with your own tools instead of delegating further.`);
|
|
236
|
+
}
|
|
237
|
+
const sessionId = typeof contextRecord.sessionId === "string"
|
|
238
|
+
? contextRecord.sessionId
|
|
239
|
+
: undefined;
|
|
240
|
+
// Open-handle conflict: an in-progress leashed leg for this agent, owned
|
|
241
|
+
// by THIS host + caller session, must be continued, not restarted. Other
|
|
242
|
+
// sessions' handles are invisible here — the pool bounds cross-session
|
|
243
|
+
// concurrency.
|
|
244
|
+
const openHandle = hasOpenIsolatedAgentHandle(host, definition.id, sessionId);
|
|
245
|
+
if (openHandle.open) {
|
|
246
|
+
return refusal(`Agent "${definition.id}" already has an in-progress investigation (handle ${openHandle.handle}); continue it via its handle instead of delegating anew.`);
|
|
247
|
+
}
|
|
248
|
+
// Per-turn cap — counted in the loop itself, per top-level generate. The
|
|
249
|
+
// count check runs before the pool wait, but the count is CONSUMED only
|
|
250
|
+
// once a run actually starts: a refusal (depth/handle/pool) must not burn
|
|
251
|
+
// one of the model's delegations. Nested scopes share the top-level
|
|
252
|
+
// turn's counters (see runWithNestedDelegationDepth).
|
|
253
|
+
const turnState = turnScope;
|
|
254
|
+
if (options.maxDelegationsPerTurn !== undefined && turnState) {
|
|
255
|
+
const used = turnState.counts.get(name) ?? 0;
|
|
256
|
+
if (used >= options.maxDelegationsPerTurn) {
|
|
257
|
+
return refusal(`Delegation limit reached: ${name} has already been called ${used}× this turn (max ${options.maxDelegationsPerTurn}). Do not call ${name} again this turn; synthesize from the investigations you already have.`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Nested delegations bypass the pool: the outer delegation already holds a
|
|
261
|
+
// slot, so queueing nested work behind a full pool would deadlock it (all
|
|
262
|
+
// slots held by outers waiting on their inners).
|
|
263
|
+
let release;
|
|
264
|
+
if (depth === 0) {
|
|
265
|
+
try {
|
|
266
|
+
release = await acquireDelegationSlot(options.poolQueueTimeoutMs ?? DEFAULT_POOL_QUEUE_TIMEOUT_MS);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return refusal(`All delegation slots are busy and the queue timed out. Do not retry ${name} immediately; continue with your own tools or synthesize from the investigations you already have.`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
// Consume the per-turn count only now that the run actually starts.
|
|
274
|
+
if (options.maxDelegationsPerTurn !== undefined && turnState) {
|
|
275
|
+
turnState.counts.set(name, (turnState.counts.get(name) ?? 0) + 1);
|
|
276
|
+
}
|
|
277
|
+
// Default task-string schema: unwrap `task`; a bare string passes
|
|
278
|
+
// through; any other object passes through whole (runIsolatedAgent
|
|
279
|
+
// JSON-stringifies structured input — never "[object Object]").
|
|
280
|
+
const input = definition.inputSchema
|
|
281
|
+
? params
|
|
282
|
+
: typeof params?.task === "string"
|
|
283
|
+
? params.task
|
|
284
|
+
: typeof params === "string"
|
|
285
|
+
? params
|
|
286
|
+
: (params ?? {});
|
|
287
|
+
const abortSignal = contextRecord
|
|
288
|
+
.abortSignal;
|
|
289
|
+
const outcome = await runIsolatedAgent(host, definition, input, {
|
|
290
|
+
...(abortSignal && { abortSignal }),
|
|
291
|
+
...(options.leg && { leg: options.leg }),
|
|
292
|
+
...(options.handleTtlMs !== undefined && {
|
|
293
|
+
handleTtlMs: options.handleTtlMs,
|
|
294
|
+
}),
|
|
295
|
+
...(options.waste && { waste: options.waste }),
|
|
296
|
+
...(options.overrides && { overrides: options.overrides }),
|
|
297
|
+
toolContext: {
|
|
298
|
+
agentDepth: depth + 1,
|
|
299
|
+
...(sessionId && { sessionId }),
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
// Bounded summary into the host loop — full records stay on the outcome
|
|
303
|
+
// for programmatic callers; the model gets the essentials.
|
|
304
|
+
return {
|
|
305
|
+
status: outcome.status,
|
|
306
|
+
...(outcome.data !== undefined && { data: outcome.data }),
|
|
307
|
+
...(outcome.content && {
|
|
308
|
+
content: outcome.content.length > DELEGATION_RESULT_CONTENT_CHARS
|
|
309
|
+
? `${outcome.content.slice(0, DELEGATION_RESULT_CONTENT_CHARS)}…`
|
|
310
|
+
: outcome.content,
|
|
311
|
+
}),
|
|
312
|
+
...(outcome.stopReason && { stopReason: outcome.stopReason }),
|
|
313
|
+
toolCallsUsed: outcome.toolExecutions.length,
|
|
314
|
+
durationMs: outcome.durationMs,
|
|
315
|
+
...(outcome.handle && { handle: outcome.handle }),
|
|
316
|
+
...(outcome.delta && { delta: outcome.delta }),
|
|
317
|
+
...(outcome.nextPlan && { nextPlan: outcome.nextPlan }),
|
|
318
|
+
...(outcome.wasteSignals && { wasteSignals: outcome.wasteSignals }),
|
|
319
|
+
...(outcome.budget && { budget: outcome.budget }),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
return refusal(`Delegation to ${name} failed: ${error instanceof Error ? error.message : String(error)}. Continue with your own tools or synthesize from what you already have.`);
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
release?.();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** Test-only: reset the process-wide pool. */
|
|
330
|
+
export function resetDelegationPoolForTests(capacity) {
|
|
331
|
+
poolCapacity = capacity ?? DEFAULT_POOL_CAPACITY;
|
|
332
|
+
poolInUse = 0;
|
|
333
|
+
poolWaiters
|
|
334
|
+
.splice(0, poolWaiters.length)
|
|
335
|
+
.forEach((w) => w.cancel(new Error("pool reset")));
|
|
336
|
+
}
|
|
337
|
+
//# sourceMappingURL=agentToolRegistrar.js.map
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
*/
|
|
32
32
|
export { Agent } from "./agent.js";
|
|
33
33
|
export { AgentNetwork } from "./agentNetwork.js";
|
|
34
|
+
export { runIsolatedAgent, continueIsolatedAgent, stopIsolatedAgent, buildMechanicalDigest, hasOpenIsolatedAgentHandle, } from "./isolatedAgentRunner.js";
|
|
35
|
+
export { registerAgentTool, beginDelegationTurn, acquireDelegationSlot, runWithNestedDelegationDepth, resetDelegationPoolForTests, } from "./agentToolRegistrar.js";
|
|
36
|
+
export { collectStructuredCandidates, recoverStructuredData, summarizeZodError, } from "./structuredRecovery.js";
|
|
34
37
|
export { directAgentTools, getAvailableToolNames, getToolsForCategory, } from "./directTools.js";
|
|
35
38
|
export { buildConfidencePrompt, buildMultiStepPlanningPrompt, buildRoutingPrompt, parseRoutingResponse, ROUTING_PROMPTS, } from "./prompts/routingPrompts.js";
|
|
36
39
|
export { AgentCoordinator, TaskDistributor } from "./coordination/index.js";
|
package/dist/lib/agent/index.js
CHANGED
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
// Core agent classes
|
|
33
33
|
export { Agent } from "./agent.js";
|
|
34
34
|
export { AgentNetwork } from "./agentNetwork.js";
|
|
35
|
+
// Isolated sub-agent runtime (N4/N5)
|
|
36
|
+
export { runIsolatedAgent, continueIsolatedAgent, stopIsolatedAgent, buildMechanicalDigest, hasOpenIsolatedAgentHandle, } from "./isolatedAgentRunner.js";
|
|
37
|
+
export { registerAgentTool, beginDelegationTurn, acquireDelegationSlot, runWithNestedDelegationDepth, resetDelegationPoolForTests, } from "./agentToolRegistrar.js";
|
|
38
|
+
export { collectStructuredCandidates, recoverStructuredData, summarizeZodError, } from "./structuredRecovery.js";
|
|
35
39
|
// Direct tools (existing)
|
|
36
40
|
export { directAgentTools, getAvailableToolNames, getToolsForCategory, } from "./directTools.js";
|
|
37
41
|
// Routing prompts and utilities
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runIsolatedAgent() — the production sub-agent runner.
|
|
3
|
+
*
|
|
4
|
+
* One function replacing what every consumer hand-rolled on top of
|
|
5
|
+
* generate(): a worker-mode instance (memory off, orchestration off,
|
|
6
|
+
* observability inherited), a tool-using research pass under the turn budget
|
|
7
|
+
* (wrap-up nudge + stall watchdog + honest stopReason), an extraction pass
|
|
8
|
+
* that ALWAYS runs tools-off on its own timeout, a structured-recovery
|
|
9
|
+
* ladder with corrective re-asks, a delivery guarantee (non-empty execution
|
|
10
|
+
* records never produce an empty result), abort chaining, a lifecycle event
|
|
11
|
+
* stream, leashed legs with TTL'd resume handles, and mechanical
|
|
12
|
+
* waste-signature detection.
|
|
13
|
+
*
|
|
14
|
+
* Behavioral contract: docs/plans/2026-07-27-isolated-agent-runner-rfc.md.
|
|
15
|
+
*/
|
|
16
|
+
import type { NeuroLink } from "../neurolink.js";
|
|
17
|
+
import type { AgentRunOptions, AgentRunOutcome, IsolatedAgentDefinition, ToolExecutionRecord } from "../types/index.js";
|
|
18
|
+
/**
|
|
19
|
+
* True when the SAME host + caller session already has an open (live)
|
|
20
|
+
* leashed handle for this agent definition.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately narrow: a handle belongs to the conversation that opened it,
|
|
23
|
+
* so the conflict only exists when the caller-supplied `sessionId` matches —
|
|
24
|
+
* a multi-tenant host must never refuse thread B because thread A has an
|
|
25
|
+
* in-flight leg (thread B cannot see, continue, or legitimately steer that
|
|
26
|
+
* handle). Callers without a stable sessionId get no conflict detection;
|
|
27
|
+
* cross-session concurrency is the delegation pool's job.
|
|
28
|
+
*/
|
|
29
|
+
export declare function hasOpenIsolatedAgentHandle(host: unknown, definitionId: string, callerSessionId?: string): {
|
|
30
|
+
open: boolean;
|
|
31
|
+
handle?: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Mechanical digest of a run's execution records: which tools ran, ok/failed
|
|
35
|
+
* counts, and bounded excerpts of successful payloads. The delivery
|
|
36
|
+
* guarantee's last line of defense — a non-empty execution record can never
|
|
37
|
+
* produce an empty result.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildMechanicalDigest(records: ToolExecutionRecord[]): {
|
|
40
|
+
kind: "mechanical-digest";
|
|
41
|
+
toolsRun: Record<string, {
|
|
42
|
+
calls: number;
|
|
43
|
+
ok: number;
|
|
44
|
+
failed: number;
|
|
45
|
+
}>;
|
|
46
|
+
excerpts: Array<{
|
|
47
|
+
toolName: string;
|
|
48
|
+
params: string;
|
|
49
|
+
resultText: string;
|
|
50
|
+
}>;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Run an isolated sub-agent: research pass with tools under the turn budget,
|
|
54
|
+
* extraction pass tools-off on its own timeout, structured recovery,
|
|
55
|
+
* delivery guarantee, abort chaining, events, and (when `options.leg` is
|
|
56
|
+
* set) leashed legs with a TTL'd resume handle.
|
|
57
|
+
*/
|
|
58
|
+
export declare function runIsolatedAgent(host: NeuroLink, definition: IsolatedAgentDefinition, input: string | Record<string, unknown>, options?: AgentRunOptions): Promise<AgentRunOutcome>;
|
|
59
|
+
/**
|
|
60
|
+
* Resume a leashed run. `guidance`, when provided, is appended as a user
|
|
61
|
+
* turn before the next leg — this is how a supervisor re-steers a drifting
|
|
62
|
+
* worker. An expired handle returns its tombstoned final outcome once.
|
|
63
|
+
* Handles are owned by the host that created them; a handle with a leg
|
|
64
|
+
* already in flight refuses a second continuation instead of racing it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function continueIsolatedAgent(host: NeuroLink, handle: string, guidance?: string): Promise<AgentRunOutcome>;
|
|
67
|
+
/**
|
|
68
|
+
* Stop a leashed run: dispose the worker and return the final outcome
|
|
69
|
+
* (mechanical digest over everything gathered so far). Refuses while a leg
|
|
70
|
+
* is in flight — cancel an in-flight leg via the run's `abortSignal` instead
|
|
71
|
+
* of disposing its worker mid-generate.
|
|
72
|
+
*/
|
|
73
|
+
export declare function stopIsolatedAgent(host: NeuroLink, handle: string): Promise<AgentRunOutcome>;
|