@juspay/neurolink 12.28.3 → 12.29.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 (45) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/dist/adapters/tts/googleTTSHandler.d.ts +71 -1
  3. package/dist/adapters/tts/googleTTSHandler.js +254 -0
  4. package/dist/browser/neurolink.min.js +399 -399
  5. package/dist/cli/commands/voices.d.ts +23 -0
  6. package/dist/cli/commands/voices.js +102 -0
  7. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  8. package/dist/cli/parser.js +3 -0
  9. package/dist/constants/enums.d.ts +13 -0
  10. package/dist/constants/enums.js +15 -0
  11. package/dist/core/baseProvider.d.ts +4 -0
  12. package/dist/core/baseProvider.js +245 -148
  13. package/dist/core/modules/structuredOutputPolicy.d.ts +27 -0
  14. package/dist/core/modules/structuredOutputPolicy.js +36 -0
  15. package/dist/core/nativeGenerateLoop.js +7 -1
  16. package/dist/neurolink.d.ts +42 -0
  17. package/dist/neurolink.js +102 -2
  18. package/dist/providers/catalog/index.generated.d.ts +1 -1
  19. package/dist/providers/catalog/index.generated.js +6 -0
  20. package/dist/providers/catalog/morph.json +97 -0
  21. package/dist/providers/catalog/novita.json +114 -0
  22. package/dist/providers/openaiChatCompletionsBase.d.ts +22 -0
  23. package/dist/providers/openaiChatCompletionsBase.js +291 -8
  24. package/dist/server/utils/validation.d.ts +34 -0
  25. package/dist/server/utils/validation.js +27 -0
  26. package/dist/server/websocket/WebSocketHandler.d.ts +4 -6
  27. package/dist/server/websocket/WebSocketHandler.js +185 -18
  28. package/dist/types/cli.d.ts +11 -0
  29. package/dist/types/generate.d.ts +15 -0
  30. package/dist/types/isolatedAgent.d.ts +16 -7
  31. package/dist/types/openaiCompatible.d.ts +6 -0
  32. package/dist/types/providerCatalog.generated.d.ts +2 -2
  33. package/dist/types/providers.d.ts +50 -3
  34. package/dist/types/server.d.ts +8 -0
  35. package/dist/types/stream.d.ts +30 -0
  36. package/dist/types/tts.d.ts +10 -0
  37. package/dist/types/utilities.d.ts +16 -3
  38. package/dist/utils/logger.d.ts +58 -13
  39. package/dist/utils/logger.js +156 -11
  40. package/dist/utils/providerHealth.d.ts +65 -1
  41. package/dist/utils/providerHealth.js +216 -54
  42. package/dist/utils/ttsProcessor.d.ts +30 -1
  43. package/dist/utils/ttsProcessor.js +76 -0
  44. package/docs-site/static/search-index.json +4 -4
  45. package/package.json +6 -1
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { WebSocketError, WebSocketConnectionError } from "../errors.js";
8
8
  import { logger } from "../../utils/logger.js";
9
+ import { WebSocketAgentRequestSchema } from "../utils/validation.js";
9
10
  /**
10
11
  * Default WebSocket configuration
11
12
  */
@@ -58,6 +59,46 @@ export class WebSocketConnectionManager {
58
59
  if (this.connections.size >= this.config.maxConnections) {
59
60
  throw new WebSocketConnectionError(`Maximum connections (${this.config.maxConnections}) reached`);
60
61
  }
62
+ // Enforce the auth contract the config already declares. `auth.required`
63
+ // has existed on WebSocketConfig (with a `strategy`) since this manager
64
+ // was written, and `handleConnection` has always accepted and stored an
65
+ // AuthenticatedUser — but nothing ever checked either, so a deployer who
66
+ // set `required: true` got exactly the same open socket as one who did
67
+ // not. That was latent while the agent routes were stubs that returned
68
+ // canned values; it stops being latent the moment `tool_call` actually
69
+ // reaches `executeTool`, because then an unauthenticated client can
70
+ // invoke any registered tool with arbitrary arguments. Refuse the
71
+ // connection rather than letting a route decide, so every handler
72
+ // registered on this manager inherits the gate.
73
+ if (this.config.auth.required && !user) {
74
+ logger.warn(`[WebSocket] Rejected unauthenticated connection to ${path} (auth.required)`);
75
+ throw new WebSocketConnectionError("Authentication required for this WebSocket endpoint");
76
+ }
77
+ // Same gap, same fix, one field over. `ServerAuthConfig.roles`/
78
+ // `permissions` and `AuthenticatedUser.roles`/`permissions` have existed
79
+ // since this manager was written — the shipped "Role-Based Access
80
+ // Control" doc example configures them on a `/ws/admin` path — but
81
+ // nothing ever consulted either, so a deployer following that example
82
+ // got an endpoint anyone authenticated could reach, regardless of role.
83
+ // Semantics match the SDK's own `hasAnyRole`/`hasAllPermissions`
84
+ // (src/lib/auth/authContext.ts): roles are an any-of allowlist,
85
+ // permissions must all be present.
86
+ const requiredRoles = this.config.auth.roles;
87
+ if (requiredRoles && requiredRoles.length > 0) {
88
+ const userRoles = user?.roles ?? [];
89
+ if (!requiredRoles.some((role) => userRoles.includes(role))) {
90
+ logger.warn(`[WebSocket] Rejected connection to ${path}: missing a required role`);
91
+ throw new WebSocketConnectionError("Insufficient role for this WebSocket endpoint");
92
+ }
93
+ }
94
+ const requiredPermissions = this.config.auth.permissions;
95
+ if (requiredPermissions && requiredPermissions.length > 0) {
96
+ const userPermissions = user?.permissions ?? [];
97
+ if (!requiredPermissions.every((permission) => userPermissions.includes(permission))) {
98
+ logger.warn(`[WebSocket] Rejected connection to ${path}: missing a required permission`);
99
+ throw new WebSocketConnectionError("Insufficient permissions for this WebSocket endpoint");
100
+ }
101
+ }
61
102
  const connection = {
62
103
  id: generateConnectionId(),
63
104
  socket,
@@ -284,6 +325,17 @@ export class WebSocketConnectionManager {
284
325
  }
285
326
  }
286
327
  }
328
+ /**
329
+ * Narrow an incoming message's raw `id` field to the one shape it is safe to
330
+ * echo back (see `WebSocketRequestId`); anything else — including a missing
331
+ * field, since most callers won't opt into correlation at all — is treated
332
+ * as "no id".
333
+ */
334
+ function readRequestId(value) {
335
+ return typeof value === "string" || typeof value === "number"
336
+ ? value
337
+ : undefined;
338
+ }
287
339
  /**
288
340
  * WebSocket message router for handling different message types
289
341
  */
@@ -309,7 +361,8 @@ export class WebSocketMessageRouter {
309
361
  catch {
310
362
  throw new WebSocketError("Invalid JSON message");
311
363
  }
312
- const { type, payload } = parsed;
364
+ const { type, payload, id } = parsed;
365
+ const requestId = readRequestId(id);
313
366
  if (!type) {
314
367
  throw new WebSocketError("Message type is required");
315
368
  }
@@ -317,7 +370,22 @@ export class WebSocketMessageRouter {
317
370
  if (!handler) {
318
371
  throw new WebSocketError(`Unknown message type: ${type}`);
319
372
  }
320
- return handler(connection, payload);
373
+ const result = await handler(connection, payload, requestId);
374
+ // Overlapping in-flight messages on one connection previously produced
375
+ // response frames with nothing tying them back to the request that
376
+ // caused them (silent when the client had more than one call open at
377
+ // once). A route that already tags its own frame — the streaming route
378
+ // does, for the intermediate frames it sends directly — is left alone;
379
+ // this only fills in the id for a plain result object that doesn't
380
+ // carry one, so a route that ignores `requestId` still gets correlated
381
+ // for free, and a client that never sends an `id` sees no change at all.
382
+ if (requestId !== undefined &&
383
+ result !== null &&
384
+ typeof result === "object" &&
385
+ !("id" in result)) {
386
+ return { ...result, id: requestId };
387
+ }
388
+ return result;
321
389
  }
322
390
  /**
323
391
  * Get registered routes
@@ -329,23 +397,110 @@ export class WebSocketMessageRouter {
329
397
  /**
330
398
  * Create a WebSocket handler for AI agent interactions
331
399
  */
332
- export function createAgentWebSocketHandler(_neurolink) {
400
+ /**
401
+ * Send one frame, reporting failure instead of throwing.
402
+ *
403
+ * A socket that has already closed throws on `send`. That matters in two
404
+ * places here: mid-stream, where an unguarded throw unwinds into `onMessage`'s
405
+ * catch, which then tries to send an error frame on the same dead socket and
406
+ * throws again — this time with nothing above it to catch, i.e. an unhandled
407
+ * rejection; and in that catch itself, for the same reason. Returning a
408
+ * boolean lets both callers stop cleanly instead.
409
+ */
410
+ function trySend(connection, payload) {
411
+ const socket = connection.socket;
412
+ try {
413
+ socket.send(JSON.stringify(payload));
414
+ return true;
415
+ }
416
+ catch {
417
+ return false;
418
+ }
419
+ }
420
+ /**
421
+ * Pull a correlation id off a raw incoming frame without depending on
422
+ * `WebSocketMessageRouter.handle` having successfully parsed it — the one
423
+ * case this is for is a message that fails *inside* `handle` (bad JSON,
424
+ * missing type, unknown route), where the router never gets far enough to
425
+ * return the id it would otherwise have tagged the response with. Failure
426
+ * here (not text, not JSON, no `id`) just means "no id", the same as if the
427
+ * client never sent one.
428
+ */
429
+ function readIncomingRequestId(message) {
430
+ if (message.type !== "text") {
431
+ return undefined;
432
+ }
433
+ try {
434
+ const parsed = JSON.parse(message.data);
435
+ return readRequestId(parsed.id);
436
+ }
437
+ catch {
438
+ return undefined;
439
+ }
440
+ }
441
+ export function createAgentWebSocketHandler(neurolink) {
333
442
  const router = new WebSocketMessageRouter();
334
443
  // Register message routes
444
+ //
445
+ // `payload` is attacker-controlled JSON (WebSocketMessageRouter.handle does
446
+ // nothing but JSON.parse it), so `generate`/`stream` validate it against
447
+ // `WebSocketAgentRequestSchema` before it reaches the SDK rather than
448
+ // trusting a bare type assertion. The schema's `options` allowlist is
449
+ // deliberately narrower than `GenerateOptions`/`StreamOptions` — most
450
+ // importantly it has no `credentials` field, so a client can no longer
451
+ // point the server's outbound request at an arbitrary `baseURL` (SSRF) or
452
+ // swap in its own provider API key.
335
453
  router.route("generate", async (connection, payload) => {
336
- const { prompt, options: _options } = payload;
337
- // TODO(#1576): Implement generate using neurolink
338
- return { type: "response", data: `Received: ${prompt}` };
454
+ const parsed = WebSocketAgentRequestSchema.safeParse(payload);
455
+ if (!parsed.success) {
456
+ throw new WebSocketError(`Invalid generate payload: ${parsed.error.issues.map((issue) => issue.message).join(", ")}`);
457
+ }
458
+ const { prompt, options } = parsed.data;
459
+ const result = await neurolink.generate({
460
+ ...options,
461
+ input: { text: prompt },
462
+ });
463
+ return { type: "response", content: result.content };
339
464
  });
340
- router.route("stream", async (connection, payload) => {
341
- const { prompt, options: _options } = payload;
342
- // TODO(#1576): Implement streaming using neurolink
343
- return { type: "stream_start", data: { prompt } };
465
+ router.route("stream", async (connection, payload, requestId) => {
466
+ const parsed = WebSocketAgentRequestSchema.safeParse(payload);
467
+ if (!parsed.success) {
468
+ throw new WebSocketError(`Invalid stream payload: ${parsed.error.issues.map((issue) => issue.message).join(", ")}`);
469
+ }
470
+ const { prompt, options } = parsed.data;
471
+ // stream_start/chunk are sent directly through trySend rather than
472
+ // returned, so — unlike generate/tool_call's single returned frame —
473
+ // they sit outside WebSocketMessageRouter.handle's own id-tagging and
474
+ // must carry the correlation id themselves.
475
+ const withRequestId = (frame) => requestId !== undefined ? { ...frame, id: requestId } : frame;
476
+ if (!trySend(connection, withRequestId({ type: "stream_start", data: { prompt } }))) {
477
+ return undefined;
478
+ }
479
+ const result = await neurolink.stream({
480
+ ...options,
481
+ input: { text: prompt },
482
+ });
483
+ let clientPresent = true;
484
+ for await (const chunk of result.stream) {
485
+ if ("content" in chunk && typeof chunk.content === "string") {
486
+ if (!trySend(connection, withRequestId({ type: "chunk", content: chunk.content }))) {
487
+ // The client is gone. Breaking runs the stream's IteratorClose,
488
+ // which now aborts the upstream request rather than leaving it in
489
+ // flight for a reader that no longer exists.
490
+ clientPresent = false;
491
+ logger.info(`[AgentWebSocket] Client went away mid-stream: ${connection.id}`);
492
+ break;
493
+ }
494
+ }
495
+ }
496
+ // Returning nothing keeps onMessage from sending a completion frame on a
497
+ // socket that just refused one.
498
+ return clientPresent ? { type: "stream_complete" } : undefined;
344
499
  });
345
500
  router.route("tool_call", async (connection, payload) => {
346
- const { toolName, args: _args } = payload;
347
- // TODO(#1576): Implement tool call using neurolink
348
- return { type: "tool_result", data: { toolName, result: null } };
501
+ const { toolName, args } = payload;
502
+ const result = await neurolink.executeTool(toolName, args);
503
+ return { type: "tool_result", toolName, result };
349
504
  });
350
505
  return {
351
506
  onOpen: async (connection) => {
@@ -358,19 +513,31 @@ export function createAgentWebSocketHandler(_neurolink) {
358
513
  }));
359
514
  },
360
515
  onMessage: async (connection, message) => {
516
+ // Best-effort only: a malformed frame still fails inside
517
+ // router.handle below (and reports as an id-less error, same as
518
+ // before), this just lets a *valid* frame's error response carry the
519
+ // same correlation id a successful response would have gotten.
520
+ const requestId = readIncomingRequestId(message);
361
521
  try {
362
522
  const result = await router.handle(connection, message);
363
523
  if (result) {
364
- const socket = connection.socket;
365
- socket.send(JSON.stringify(result));
524
+ trySend(connection, result);
366
525
  }
367
526
  }
368
527
  catch (error) {
369
- const socket = connection.socket;
370
- socket.send(JSON.stringify({
528
+ // Guarded: this catch is the last line of defence, so a throw from
529
+ // the send itself — which is exactly what a closed socket does — has
530
+ // nothing above it and would surface as an unhandled rejection.
531
+ const errorFrame = {
371
532
  type: "error",
372
533
  error: error.message,
373
- }));
534
+ };
535
+ if (requestId !== undefined) {
536
+ errorFrame.id = requestId;
537
+ }
538
+ if (!trySend(connection, errorFrame)) {
539
+ logger.warn(`[AgentWebSocket] Could not deliver error frame, socket closed: ${connection.id}`);
540
+ }
374
541
  }
375
542
  },
376
543
  onClose: async (connection, code, reason) => {
@@ -1753,6 +1753,17 @@ export type CliClassifierRouterFlags = {
1753
1753
  /** LLM-classifier hard timeout in ms (--classifier-timeout). */
1754
1754
  classifierTimeout?: number;
1755
1755
  };
1756
+ /**
1757
+ * `neurolink voices` arguments — TTS voice discovery.
1758
+ */
1759
+ export type CliVoicesCommandArgs = {
1760
+ /** TTS provider whose voices to list (e.g. google-ai, openai-tts). */
1761
+ provider: string;
1762
+ /** Optional language filter passed through to the provider (e.g. en-US). */
1763
+ language?: string;
1764
+ /** Emit the raw list as JSON instead of a table. */
1765
+ json?: boolean;
1766
+ };
1756
1767
  /**
1757
1768
  * Agent command arguments for multi-agent orchestration
1758
1769
  */
@@ -1453,6 +1453,21 @@ export type TextGenerationOptions = {
1453
1453
  */
1454
1454
  outputFormat?: VisionImageOutputFormat;
1455
1455
  };
1456
+ /**
1457
+ * Video processing options (mirrors `GenerateOptions.videoOptions`). Never
1458
+ * declared here before, so `buildGenerateTextOptions` had nowhere to
1459
+ * forward the caller's setting even once the allowlist itself named it.
1460
+ */
1461
+ videoOptions?: {
1462
+ /** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
1463
+ frames?: number;
1464
+ /** Frame encoder quality, clamped to 1-100. Default 80. */
1465
+ quality?: number;
1466
+ /** Frame encoding. Default jpeg. */
1467
+ format?: "jpeg" | "png";
1468
+ /** Not implemented yet (#433) — warns rather than silently doing nothing. */
1469
+ transcribeAudio?: boolean;
1470
+ };
1456
1471
  enableSummarization?: boolean;
1457
1472
  /**
1458
1473
  * Skip injecting tool schemas into the system prompt.
@@ -43,13 +43,22 @@ export type WorkerInstanceOptions = {
43
43
  * Log bridge sink. Fire-and-forget: listener errors never disrupt the
44
44
  * worker.
45
45
  *
46
- * KNOWN LIMITATION: the underlying NeuroLink logger is process-global with
47
- * a single active emitter, so this bridge receives ALL NeuroLink log
48
- * events emitted in the process (host, other workers, MCP) for the
49
- * worker's lifetime, each stamped with this worker's `tag`. The tag
50
- * identifies which bridge forwarded the event, not which instance emitted
51
- * it. Per-instance log attribution requires per-instance logger routing —
52
- * tracked as follow-up work in the RFC.
46
+ * Receives only log events this worker emitted. The logger routes per
47
+ * instance: the worker's `generate` / `stream` / `generateText` run their
48
+ * bodies inside an AsyncLocalStorage scope carrying the worker's id, and
49
+ * this bridge is subscribed to that id — so a sibling worker's, the host's
50
+ * or a background MCP reconnect's logs never arrive here, and `tag` is
51
+ * true attribution rather than "whichever bridge forwarded it".
52
+ *
53
+ * Two things stay outside the scope, by construction:
54
+ * - Logs emitted while a **consumer drains a returned stream**. Iteration
55
+ * happens in the consumer's async context, after `stream()` resolved.
56
+ * - Logs emitted **outside any call** — construction, background MCP
57
+ * reconnects, module init. These are unattributed rather than charged to
58
+ * an arbitrary instance.
59
+ *
60
+ * A process-wide sink (`logger.setEventEmitter`) still sees everything, so
61
+ * a host bridge is unaffected by this narrowing.
53
62
  */
54
63
  onLog?: (event: WorkerLogEvent) => void;
55
64
  /**
@@ -246,6 +246,12 @@ export type StreamLoopArgs = {
246
246
  totalTokens: number;
247
247
  }) => void;
248
248
  resolveFinish: (reason: string) => void;
249
+ /**
250
+ * `response_format` for this turn's requests, computed once up front from
251
+ * `options.schema` (see `suppressResponseFormatWithTools`). Absent when no
252
+ * schema was requested, or when tools suppress it.
253
+ */
254
+ responseFormat?: OpenAICompatResponseFormat;
249
255
  };
250
256
  export type OpenAICompatBuildBodyArgs = {
251
257
  modelId: string;
@@ -1,2 +1,2 @@
1
- export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingface" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
2
- export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingFace" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
1
+ export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingface" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "morph" | "novita" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
2
+ export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingFace" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "morph" | "novita" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
@@ -229,6 +229,14 @@ export type NeurolinkCredentials = {
229
229
  apiKey?: string;
230
230
  baseURL?: string;
231
231
  };
232
+ morph?: {
233
+ apiKey?: string;
234
+ baseURL?: string;
235
+ };
236
+ novita?: {
237
+ apiKey?: string;
238
+ baseURL?: string;
239
+ };
232
240
  perplexity?: {
233
241
  apiKey?: string;
234
242
  baseURL?: string;
@@ -1761,8 +1769,31 @@ export type ProviderHealthCheckOptions = {
1761
1769
  includeConnectivityTest?: boolean;
1762
1770
  includeModelValidation?: boolean;
1763
1771
  cacheResults?: boolean;
1772
+ /**
1773
+ * Max age (ms) of a cached health-check result before it is treated as
1774
+ * stale. Only consulted when `cacheResults` is true — with
1775
+ * `cacheResults: false` this option has no effect. It does not affect the
1776
+ * circuit breaker's blacklist expiry, which uses its own fixed window
1777
+ * independent of any caller's `maxCacheAge`.
1778
+ */
1764
1779
  maxCacheAge?: number;
1765
1780
  };
1781
+ /**
1782
+ * Outcome of a provider-specific config check's own outbound runtime probe
1783
+ * (LiteLLM's `/v1/models`, Ollama's `/api/tags` availability check) — the
1784
+ * request `checkLiteLLMConfig`/`checkOllamaConfig` make from inside step 1
1785
+ * (`checkEnvironmentConfiguration`), independent of the step-3 connectivity
1786
+ * test. `checkProviderHealth`'s circuit breaker needs to know whether this
1787
+ * probe ran at all (a blacklisted provider skips it — `ran: false`) and,
1788
+ * if it ran, whether it failed, so a dead local proxy counts toward the
1789
+ * breaker the same way a failing step-3 probe does. `ran: false` must never
1790
+ * move the breaker either way — it is not evidence the provider is up OR
1791
+ * down.
1792
+ */
1793
+ export type ProviderRuntimeProbeOutcome = {
1794
+ ran: boolean;
1795
+ failed: boolean;
1796
+ };
1766
1797
  /**
1767
1798
  * Amazon Bedrock specific types
1768
1799
  */
@@ -2023,14 +2054,30 @@ export type ProviderDescriptor = {
2023
2054
  /** How ProviderHealthChecker should verify this provider is reachable. */
2024
2055
  healthCheck: "env-only" | "models-probe" | "live-generate";
2025
2056
  /**
2026
- * Membership + order in the default health sweep
2057
+ * ORDER (not membership) in the default health sweep
2027
2058
  * (`ProviderHealthChecker.checkAllProvidersHealth` with no explicit
2028
2059
  * list). Lower number = checked and reported first; the sweep's array
2029
2060
  * order is behaviour for its first-healthy fallback consumers. Absent =
2030
- * not part of the default sweep. Replaces the hand-maintained 8-provider
2031
- * array that lived in providerHealth.ts.
2061
+ * sorted after every explicitly-prioritized descriptor, in this file's
2062
+ * declaration order (a stable sort, so ties never reorder). Originally
2063
+ * this field ALSO controlled membership (absent = excluded), which
2064
+ * silently dropped every provider added without it from the sweep —
2065
+ * issue #1305, ~30 of ~38 registered providers, undocumented. Membership
2066
+ * is now `excludeFromHealthSweep` below, which every new descriptor
2067
+ * satisfies by default without needing this field at all.
2032
2068
  */
2033
2069
  defaultHealthSweepPriority?: number;
2070
+ /**
2071
+ * Opt a provider OUT of `checkAllProvidersHealth`'s default sweep. Every
2072
+ * registered descriptor participates by default — a newly added provider
2073
+ * needs no action to be included, which is the fix for issue #1305
2074
+ * (silent, undocumented exclusion of most providers). Set this only for a
2075
+ * descriptor that genuinely should never appear in a health rollup; no
2076
+ * current descriptor sets it. `checkProviderHealth()` for a single named
2077
+ * provider is unaffected either way — this only gates the "all
2078
+ * providers" sweep.
2079
+ */
2080
+ excludeFromHealthSweep?: true;
2034
2081
  /**
2035
2082
  * Preference rank for `getBestHealthyProvider`'s default auto-selection
2036
2083
  * (lower = tried first). Deliberately a SEPARATE ordering from the sweep:
@@ -682,6 +682,14 @@ export type WebSocketMessage = {
682
682
  data: string | ArrayBuffer;
683
683
  timestamp: number;
684
684
  };
685
+ /**
686
+ * Client-supplied correlation id on an agent-protocol WebSocket message
687
+ * (`{ type, payload, id }`). Echoed back on the matching response frame so a
688
+ * caller with more than one in-flight message on a connection can tell which
689
+ * frame answers which request; optional and ignored entirely by a client
690
+ * that never sends one.
691
+ */
692
+ export type WebSocketRequestId = string | number;
685
693
  /**
686
694
  * Authenticated user information
687
695
  */
@@ -838,6 +838,36 @@ export type StreamResult = {
838
838
  stopReason?: GenerateStopReason;
839
839
  rawFinishReason?: string;
840
840
  stepsUsed?: number;
841
+ /**
842
+ * Parsed structured output for a `stream({ schema })` turn, available
843
+ * AFTER the stream has been drained.
844
+ *
845
+ * Lives on metadata for the same reason `finishReason` above does: it is a
846
+ * mutable reference the loop fills in, so result-object spreads in stream
847
+ * wrappers cannot snapshot it before it resolves. A plain field rather than
848
+ * a promise, deliberately — when a middleware short-circuits the request
849
+ * and the loop never runs, an unresolved promise would hang every reader,
850
+ * whereas an absent field is simply absent. Readers must tolerate that:
851
+ * absence means the model never produced the object.
852
+ */
853
+ structuredData?: unknown;
854
+ /**
855
+ * Tokens spent by the tool-free re-ask that produced `structuredData`,
856
+ * when one was needed.
857
+ *
858
+ * That re-ask is a second, separately-billed model call, and the stream's
859
+ * own `usage` has already resolved by the time it runs — so folding these
860
+ * tokens into it would mutate a value a caller may have read, and leaving
861
+ * them out entirely would under-report what the turn cost. Reported here
862
+ * instead: same delivery as `structuredData`, filled at the same moment,
863
+ * read at the same moment. Absent when no re-ask was needed, which is the
864
+ * common case. The generate path accounts for its equivalent re-ask
865
+ * inline, since there the usage has not been handed out yet.
866
+ */
867
+ structuredDataUsage?: {
868
+ inputTokens: number;
869
+ outputTokens: number;
870
+ };
841
871
  thoughtSignature?: string;
842
872
  thoughts?: Array<{
843
873
  id?: string;
@@ -164,6 +164,16 @@ export declare const VALID_AUDIO_FORMATS: readonly TTSAudioFormat[];
164
164
  export declare const VALID_TTS_QUALITIES: readonly TTSQuality[];
165
165
  /** Valid Google TTS audio formats */
166
166
  export type GoogleAudioEncoding = "MP3" | "LINEAR16" | "OGG_OPUS";
167
+ /**
168
+ * Audio encodings Google's *streaming* synthesis endpoint accepts.
169
+ *
170
+ * Deliberately a separate type from {@link GoogleAudioEncoding}: the
171
+ * batch and streaming endpoints do not accept the same set. `MP3` and
172
+ * `LINEAR16` are valid for batch and rejected by streaming with
173
+ * `INVALID_ARGUMENT: Unsupported audio encoding`, while `PCM` — raw
174
+ * 16-bit signed LE, headerless — exists only on the streaming side.
175
+ */
176
+ export type GoogleStreamingAudioEncoding = "PCM" | "OGG_OPUS";
167
177
  /**
168
178
  * Type guard to check if an object is a TTSResult
169
179
  */
@@ -65,9 +65,24 @@ export type LogEntry = {
65
65
  /** Optional additional data associated with the log entry (objects, arrays, etc.) */
66
66
  data?: unknown;
67
67
  };
68
+ /**
69
+ * Minimal emitter shape the logger forwards `"log-event"` payloads to.
70
+ *
71
+ * Structurally satisfied by `node:events`' `EventEmitter` and by NeuroLink's
72
+ * own typed emitter, so a sink can be either.
73
+ */
74
+ export type LogEventEmitter = {
75
+ emit: (event: string, ...args: unknown[]) => boolean;
76
+ };
68
77
  /**
69
78
  * Logger interface matching the logger object shape
70
79
  * Used for SDK tool contexts and other components that need a logger
80
+ *
81
+ * Deliberately a subset of the real logger: the per-instance routing methods
82
+ * (`runInInstanceScope`, `addScopedEventEmitter`, …) are internal plumbing
83
+ * between the SDK entry points and the logger, and this type is a structural
84
+ * contract a caller can satisfy — `SDKToolContext.logger`. Adding required
85
+ * members here would break anyone constructing that context themselves.
71
86
  */
72
87
  export type Logger = {
73
88
  debug: (...args: unknown[]) => void;
@@ -79,9 +94,7 @@ export type Logger = {
79
94
  setLogLevel: (level: LogLevel) => void;
80
95
  getLogs: (level?: LogLevel) => LogEntry[];
81
96
  clearLogs: () => void;
82
- setEventEmitter: (emitter: {
83
- emit: (event: string, ...args: unknown[]) => boolean;
84
- }) => void;
97
+ setEventEmitter: (emitter: LogEventEmitter) => void;
85
98
  clearEventEmitter: () => void;
86
99
  };
87
100
  export type StructuredError = {
@@ -13,7 +13,7 @@
13
13
  * - Structured data support for complex objects
14
14
  * - Tabular data display
15
15
  */
16
- import type { LogEntry, LogLevel } from "../types/index.js";
16
+ import type { LogEntry, LogEventEmitter, LogLevel } from "../types/index.js";
17
17
  declare class NeuroLinkLogger {
18
18
  private logLevel;
19
19
  private logs;
@@ -21,6 +21,7 @@ declare class NeuroLinkLogger {
21
21
  private isDebugMode;
22
22
  private diagnosticsToStderr;
23
23
  private eventEmitter?;
24
+ private readonly scopedEmitters;
24
25
  constructor();
25
26
  /**
26
27
  * Sets the event emitter that will receive log events.
@@ -28,9 +29,7 @@ declare class NeuroLinkLogger {
28
29
  *
29
30
  * @param emitter - The event emitter instance
30
31
  */
31
- setEventEmitter(emitter: {
32
- emit: (event: string, ...args: unknown[]) => boolean;
33
- }): void;
32
+ setEventEmitter(emitter: LogEventEmitter): void;
34
33
  /**
35
34
  * Clears the event emitter reference.
36
35
  * Should be called when a NeuroLink instance is disposed to prevent memory
@@ -40,9 +39,54 @@ declare class NeuroLinkLogger {
40
39
  *
41
40
  * @param ifEmitter - When provided, clear only if it is the current emitter
42
41
  */
43
- clearEventEmitter(ifEmitter?: {
44
- emit: (event: string, ...args: unknown[]) => boolean;
45
- }): void;
42
+ clearEventEmitter(ifEmitter?: LogEventEmitter): void;
43
+ /**
44
+ * Runs `fn` with every log call beneath it attributed to `instanceId`.
45
+ *
46
+ * Nesting is safe and expected: a host turn that delegates to a worker ends
47
+ * up with the worker's id on top for the duration of the worker's call, and
48
+ * the host's id restored afterwards. Re-entering with the same id is a
49
+ * no-op in effect.
50
+ *
51
+ * @param instanceId - Identifier of the emitting NeuroLink instance
52
+ * @param fn - Work to run inside the scope
53
+ * @returns Whatever `fn` returns
54
+ */
55
+ runInInstanceScope<T>(instanceId: string, fn: () => T): T;
56
+ /**
57
+ * The instance id currently attributed, or undefined outside any scope.
58
+ */
59
+ getInstanceScope(): string | undefined;
60
+ /**
61
+ * Subscribes `emitter` to log events emitted by one instance only.
62
+ *
63
+ * Unlike {@link setEventEmitter} — a single process-wide sink that receives
64
+ * everything — a scoped emitter receives only events logged inside that
65
+ * instance's {@link runInInstanceScope}. Several emitters may share an id.
66
+ *
67
+ * @param instanceId - Instance whose events this emitter should receive
68
+ * @param emitter - The sink to subscribe
69
+ */
70
+ addScopedEventEmitter(instanceId: string, emitter: LogEventEmitter): void;
71
+ /**
72
+ * Unsubscribes a scoped emitter. Must be called when the owning instance is
73
+ * disposed, or the emitter — and everything it closes over — is retained by
74
+ * the process-global logger for the lifetime of the process.
75
+ *
76
+ * @param instanceId - Instance the emitter was registered against
77
+ * @param emitter - The sink to unsubscribe
78
+ */
79
+ removeScopedEventEmitter(instanceId: string, emitter: LogEventEmitter): void;
80
+ /**
81
+ * Drops every scoped emitter registered against an instance.
82
+ *
83
+ * Called from `NeuroLink.dispose()`: the registry lives on the
84
+ * process-global logger, so an instance that goes away without clearing its
85
+ * own entry would keep its sinks — and their closures — reachable forever.
86
+ *
87
+ * @param instanceId - Instance whose sinks should be dropped
88
+ */
89
+ clearScopedEventEmitters(instanceId: string): void;
46
90
  /**
47
91
  * Sets the minimum log level that will be processed and output.
48
92
  * Log messages with a level lower than this will be ignored.
@@ -234,12 +278,13 @@ export declare const logger: {
234
278
  setDiagnosticsToStderr: (enabled: boolean) => void;
235
279
  getLogs: (level?: LogLevel) => LogEntry[];
236
280
  clearLogs: () => void;
237
- setEventEmitter: (emitter: {
238
- emit: (event: string, ...args: unknown[]) => boolean;
239
- }) => void;
240
- clearEventEmitter: (ifEmitter?: {
241
- emit: (event: string, ...args: unknown[]) => boolean;
242
- }) => void;
281
+ setEventEmitter: (emitter: LogEventEmitter) => void;
282
+ clearEventEmitter: (ifEmitter?: LogEventEmitter) => void;
283
+ runInInstanceScope: <T>(instanceId: string, fn: () => T) => T;
284
+ getInstanceScope: () => string | undefined;
285
+ addScopedEventEmitter: (instanceId: string, emitter: LogEventEmitter) => void;
286
+ removeScopedEventEmitter: (instanceId: string, emitter: LogEventEmitter) => void;
287
+ clearScopedEventEmitters: (instanceId: string) => void;
243
288
  };
244
289
  /**
245
290
  * MCP compatibility exports - all use the same unified logger instance.