@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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [10.6.0](https://github.com/juspay/neurolink/compare/v10.5.3...v10.6.0) (2026-07-27)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- **(agent):** add isolated sub-agent runtime and host-loop delegation ([e62390f](https://github.com/juspay/neurolink/commit/e62390f2348d492a21703e7df734de2e23e543e5))
|
|
6
|
+
|
|
1
7
|
## [10.5.3](https://github.com/juspay/neurolink/compare/v10.5.2...v10.5.3) (2026-07-26)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/dist/agent/agent.js
CHANGED
|
@@ -108,6 +108,12 @@ export class Agent {
|
|
|
108
108
|
timestamp: startTime,
|
|
109
109
|
});
|
|
110
110
|
try {
|
|
111
|
+
// No ghost runs: an already-aborted parent stops the agent before any
|
|
112
|
+
// model call is made. Typed so callers can branch on the ABORT
|
|
113
|
+
// category instead of parsing free text.
|
|
114
|
+
if (options?.abortSignal?.aborted) {
|
|
115
|
+
throw ErrorFactory.aborted(new Error("Agent execution aborted before start"));
|
|
116
|
+
}
|
|
111
117
|
// Validate input if schema provided
|
|
112
118
|
if (this.inputSchema && typeof input !== "string") {
|
|
113
119
|
const validation = this.inputSchema.safeParse(input);
|
|
@@ -149,19 +155,14 @@ export class Agent {
|
|
|
149
155
|
contentLength: result.content?.length || 0,
|
|
150
156
|
toolsUsed: result.toolsUsed?.length || 0,
|
|
151
157
|
});
|
|
152
|
-
// Pass through toolExecutions, adding duration (not provided by generate())
|
|
153
|
-
const toolExecutions = result.toolExecutions?.map((te) => ({
|
|
154
|
-
name: te.name,
|
|
155
|
-
input: te.input,
|
|
156
|
-
output: te.output,
|
|
157
|
-
duration: 0,
|
|
158
|
-
}));
|
|
159
158
|
const agentResult = {
|
|
160
159
|
content: result.content || "",
|
|
161
160
|
object: parsedOutput,
|
|
162
161
|
usage: result.usage,
|
|
163
162
|
toolsUsed: result.toolsUsed,
|
|
164
|
-
|
|
163
|
+
// Real per-call records straight from the tool loop (N2)
|
|
164
|
+
toolExecutions: result.toolExecutions,
|
|
165
|
+
stopReason: result.stopReason,
|
|
165
166
|
duration,
|
|
166
167
|
status: "success",
|
|
167
168
|
agentId: this.id,
|
|
@@ -219,6 +220,10 @@ export class Agent {
|
|
|
219
220
|
traceId,
|
|
220
221
|
};
|
|
221
222
|
try {
|
|
223
|
+
// Mirror execute(): an already-aborted parent never starts the stream.
|
|
224
|
+
if (options?.abortSignal?.aborted) {
|
|
225
|
+
throw ErrorFactory.aborted(new Error("Agent stream aborted before start"));
|
|
226
|
+
}
|
|
222
227
|
// Validate input if schema provided
|
|
223
228
|
if (this.inputSchema && typeof input !== "string") {
|
|
224
229
|
const validation = this.inputSchema.safeParse(input);
|
|
@@ -367,6 +372,20 @@ export class Agent {
|
|
|
367
372
|
...(this.tools && this.tools.length > 0 && { toolFilter: this.tools }),
|
|
368
373
|
maxSteps: options?.maxSteps ?? this.maxSteps,
|
|
369
374
|
requestId: traceId,
|
|
375
|
+
// Turn budget + cancellation, forwarded verbatim to generate() — the
|
|
376
|
+
// machinery (wrap-up nudge, stall watchdog, honest stopReason) lives
|
|
377
|
+
// in the tool loop; the agent layer just passes it through.
|
|
378
|
+
...(options?.abortSignal && { abortSignal: options.abortSignal }),
|
|
379
|
+
...(options?.timeout !== undefined && { timeout: options.timeout }),
|
|
380
|
+
...(options?.turnTimeoutMs !== undefined && {
|
|
381
|
+
turnTimeoutMs: options.turnTimeoutMs,
|
|
382
|
+
}),
|
|
383
|
+
...(options?.wrapupTimeLeadMs !== undefined && {
|
|
384
|
+
wrapupTimeLeadMs: options.wrapupTimeLeadMs,
|
|
385
|
+
}),
|
|
386
|
+
...(options?.stallTimeoutMs !== undefined && {
|
|
387
|
+
stallTimeoutMs: options.stallTimeoutMs,
|
|
388
|
+
}),
|
|
370
389
|
context: {
|
|
371
390
|
agentId: this.id,
|
|
372
391
|
agentName: this.name,
|
|
@@ -390,6 +409,18 @@ export class Agent {
|
|
|
390
409
|
// toolFilter delegates to BaseProvider.applyToolFiltering() natively
|
|
391
410
|
...(this.tools && this.tools.length > 0 && { toolFilter: this.tools }),
|
|
392
411
|
maxSteps: options?.maxSteps ?? this.maxSteps,
|
|
412
|
+
// Turn budget + cancellation, forwarded verbatim to stream()
|
|
413
|
+
...(options?.abortSignal && { abortSignal: options.abortSignal }),
|
|
414
|
+
...(options?.timeout !== undefined && { timeout: options.timeout }),
|
|
415
|
+
...(options?.turnTimeoutMs !== undefined && {
|
|
416
|
+
turnTimeoutMs: options.turnTimeoutMs,
|
|
417
|
+
}),
|
|
418
|
+
...(options?.wrapupTimeLeadMs !== undefined && {
|
|
419
|
+
wrapupTimeLeadMs: options.wrapupTimeLeadMs,
|
|
420
|
+
}),
|
|
421
|
+
...(options?.stallTimeoutMs !== undefined && {
|
|
422
|
+
stallTimeoutMs: options.stallTimeoutMs,
|
|
423
|
+
}),
|
|
393
424
|
context: {
|
|
394
425
|
agentId: this.id,
|
|
395
426
|
agentName: this.name,
|
|
@@ -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,336 @@
|
|
|
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
|
+
}
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -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/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
|