@juspay/neurolink 10.5.2 → 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.
Files changed (94) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/agent/agent.js +39 -8
  3. package/dist/agent/agentNetwork.js +62 -20
  4. package/dist/agent/agentToolRegistrar.d.ts +48 -0
  5. package/dist/agent/agentToolRegistrar.js +336 -0
  6. package/dist/agent/index.d.ts +3 -0
  7. package/dist/agent/index.js +4 -0
  8. package/dist/agent/isolatedAgentRunner.d.ts +73 -0
  9. package/dist/agent/isolatedAgentRunner.js +946 -0
  10. package/dist/agent/structuredRecovery.d.ts +30 -0
  11. package/dist/agent/structuredRecovery.js +104 -0
  12. package/dist/browser/neurolink.min.js +414 -398
  13. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  14. package/dist/core/baseProvider.d.ts +8 -0
  15. package/dist/core/baseProvider.js +40 -19
  16. package/dist/core/modules/GenerationHandler.d.ts +2 -6
  17. package/dist/core/modules/GenerationHandler.js +10 -1
  18. package/dist/core/toolExecutionRecorder.d.ts +76 -0
  19. package/dist/core/toolExecutionRecorder.js +261 -0
  20. package/dist/evaluation/contextBuilder.js +6 -6
  21. package/dist/lib/agent/agent.js +39 -8
  22. package/dist/lib/agent/agentNetwork.js +62 -20
  23. package/dist/lib/agent/agentToolRegistrar.d.ts +48 -0
  24. package/dist/lib/agent/agentToolRegistrar.js +337 -0
  25. package/dist/lib/agent/index.d.ts +3 -0
  26. package/dist/lib/agent/index.js +4 -0
  27. package/dist/lib/agent/isolatedAgentRunner.d.ts +73 -0
  28. package/dist/lib/agent/isolatedAgentRunner.js +947 -0
  29. package/dist/lib/agent/structuredRecovery.d.ts +30 -0
  30. package/dist/lib/agent/structuredRecovery.js +105 -0
  31. package/dist/lib/core/baseProvider.d.ts +8 -0
  32. package/dist/lib/core/baseProvider.js +40 -19
  33. package/dist/lib/core/modules/GenerationHandler.d.ts +2 -6
  34. package/dist/lib/core/modules/GenerationHandler.js +10 -1
  35. package/dist/lib/core/toolExecutionRecorder.d.ts +76 -0
  36. package/dist/lib/core/toolExecutionRecorder.js +262 -0
  37. package/dist/lib/evaluation/contextBuilder.js +6 -6
  38. package/dist/lib/models/modelRegistry.d.ts +23 -0
  39. package/dist/lib/models/modelRegistry.js +62 -1
  40. package/dist/lib/models/modelResolver.js +1 -0
  41. package/dist/lib/neurolink.d.ts +80 -1
  42. package/dist/lib/neurolink.js +199 -37
  43. package/dist/lib/providers/amazonBedrock.js +15 -2
  44. package/dist/lib/providers/anthropic.js +23 -11
  45. package/dist/lib/providers/googleAiStudio.js +8 -1
  46. package/dist/lib/providers/googleNativeGemini3.d.ts +2 -0
  47. package/dist/lib/providers/googleNativeGemini3.js +8 -1
  48. package/dist/lib/providers/googleVertex.js +56 -16
  49. package/dist/lib/providers/openaiChatCompletionsClient.js +18 -5
  50. package/dist/lib/tasks/autoresearchTaskExecutor.js +7 -6
  51. package/dist/lib/tasks/taskExecutor.js +2 -5
  52. package/dist/lib/types/agentNetwork.d.ts +23 -8
  53. package/dist/lib/types/generate.d.ts +62 -5
  54. package/dist/lib/types/index.d.ts +1 -0
  55. package/dist/lib/types/index.js +1 -0
  56. package/dist/lib/types/isolatedAgent.d.ts +365 -0
  57. package/dist/lib/types/isolatedAgent.js +12 -0
  58. package/dist/lib/types/model.d.ts +8 -0
  59. package/dist/lib/types/stream.d.ts +3 -1
  60. package/dist/lib/types/task.d.ts +3 -3
  61. package/dist/lib/utils/logger.d.ts +12 -3
  62. package/dist/lib/utils/logger.js +11 -3
  63. package/dist/lib/utils/transformationUtils.d.ts +14 -0
  64. package/dist/lib/utils/transformationUtils.js +33 -10
  65. package/dist/lib/voice/livekit/realtimeVoiceAgent.js +6 -3
  66. package/dist/models/modelRegistry.d.ts +23 -0
  67. package/dist/models/modelRegistry.js +62 -1
  68. package/dist/models/modelResolver.js +1 -0
  69. package/dist/neurolink.d.ts +80 -1
  70. package/dist/neurolink.js +199 -37
  71. package/dist/providers/amazonBedrock.js +15 -2
  72. package/dist/providers/anthropic.js +23 -11
  73. package/dist/providers/googleAiStudio.js +8 -1
  74. package/dist/providers/googleNativeGemini3.d.ts +2 -0
  75. package/dist/providers/googleNativeGemini3.js +8 -1
  76. package/dist/providers/googleVertex.js +56 -16
  77. package/dist/providers/openaiChatCompletionsClient.js +18 -5
  78. package/dist/tasks/autoresearchTaskExecutor.js +7 -6
  79. package/dist/tasks/taskExecutor.js +2 -5
  80. package/dist/types/agentNetwork.d.ts +23 -8
  81. package/dist/types/generate.d.ts +62 -5
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/index.js +1 -0
  84. package/dist/types/isolatedAgent.d.ts +365 -0
  85. package/dist/types/isolatedAgent.js +11 -0
  86. package/dist/types/model.d.ts +8 -0
  87. package/dist/types/stream.d.ts +3 -1
  88. package/dist/types/task.d.ts +3 -3
  89. package/dist/utils/logger.d.ts +12 -3
  90. package/dist/utils/logger.js +11 -3
  91. package/dist/utils/transformationUtils.d.ts +14 -0
  92. package/dist/utils/transformationUtils.js +33 -10
  93. package/dist/voice/livekit/realtimeVoiceAgent.js +6 -3
  94. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
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
+
7
+ ## [10.5.3](https://github.com/juspay/neurolink/compare/v10.5.2...v10.5.3) (2026-07-26)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **(ci):** raise yama floor to v2.7.2 for the 5m litellm timeout ([0dcf295](https://github.com/juspay/neurolink/commit/0dcf295547aaabfcaa95da2956f5c045d909db9b))
12
+
1
13
  ## [10.5.2](https://github.com/juspay/neurolink/compare/v10.5.1...v10.5.2) (2026-07-24)
2
14
 
3
15
  ### Bug Fixes
@@ -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
- toolExecutions,
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
- const schema = z.object({
175
- task: z.string().describe("The task to delegate to this agent"),
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
- logger.debug(`[AgentNetwork:${this.id}] Delegating to agent: ${capturedAgent.name}`, { task: params.task.slice(0, 100) });
182
- const result = await capturedAgent.execute(params.task);
183
- return {
184
- agentId: capturedId,
185
- content: result.content,
186
- status: result.status,
187
- error: result.error,
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.name.startsWith("agent_")
218
- ? exec.name.slice("agent_".length)
219
- : exec.name;
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.input,
229
- output: exec.output,
230
- duration: 0, // individual step timing not available from generate()
231
- timestamp: startTime,
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 toolExecutions = (result.toolExecutions ?? []);
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
+ }
@@ -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";
@@ -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