@juspay/neurolink
Version:
The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002
375 lines (374 loc) • 15.2 kB
TypeScript
/**
* Isolated sub-agent runtime types: worker-mode instances, the
* runIsolatedAgent() two-pass runner, leashed legs/handles, waste detection,
* and host-loop delegation (registerAgentTool).
*
* Design contract: no product policy lives here — every tunable is an option
* with a sane default, and all shapes were validated against the three
* production consumers (Curator's research + log-analysis workers, Yama's
* ContextExplorerService). See docs/plans/2026-07-27-isolated-agent-runner-rfc.md.
*/
import type { z } from "zod";
import type { TokenUsage } from "./analytics.js";
import type { AgentDefinition } from "./agentNetwork.js";
import type { GenerateStopReason, ToolExecutionRecord } from "./generate.js";
/**
* A log event forwarded by a worker instance's log bridge.
*/
export type WorkerLogEvent = {
/** Caller-supplied tag identifying which worker produced the event. */
tag: string;
/** Log level ("debug" | "info" | "warn" | "error"). */
level: string;
/** Log message. */
message: string;
/** Epoch milliseconds. */
timestamp: number;
/** Structured payload attached to the log call, if any. */
data?: unknown;
};
/**
* Options for `NeuroLink.createWorkerInstance()`.
*
* A worker instance is a sub-agent-grade NeuroLink: conversation memory off,
* orchestration off, observability inherited from the creating instance with
* `autoDetectExternalProvider: true` + `skipLangfuseSpanProcessor: true`
* (so worker spans join the host's tracer without duplicate Langfuse
* exports), and an internal log bridge attached with a caller-supplied tag.
*/
export type WorkerInstanceOptions = {
/** Tag stamped on every forwarded log event (default "worker"). */
logTag?: string;
/**
* Log bridge sink. Fire-and-forget: listener errors never disrupt the
* worker.
*
* Receives only log events this worker emitted. The logger routes per
* instance: the worker's `generate` / `stream` / `generateText` run their
* bodies inside an AsyncLocalStorage scope carrying the worker's id, and
* this bridge is subscribed to that id — so a sibling worker's, the host's
* or a background MCP reconnect's logs never arrive here, and `tag` is
* true attribution rather than "whichever bridge forwarded it".
*
* Two things stay outside the scope, by construction:
* - Logs emitted while a **consumer drains a returned stream**. Iteration
* happens in the consumer's async context, after `stream()` resolved.
* - Logs emitted **outside any call** — construction, background MCP
* reconnects, module init. These are unattributed rather than charged to
* an arbitrary instance.
*
* A process-wide sink (`logger.setEventEmitter`) still sees everything, so
* a host bridge is unaffected by this narrowing.
*/
onLog?: (event: WorkerLogEvent) => void;
/**
* Share the creating instance's tool registry (custom tools + in-memory
* MCP servers) so the worker calls tools through the host's existing
* connections. Default: true.
*/
shareToolRegistry?: boolean;
/**
* Extra constructor config merged into the worker (e.g. `credentials`,
* `tools`, `modelPool`). Worker-mode fields (memory off, orchestration
* off, observability flags) always win over this merge.
*/
config?: Record<string, unknown>;
};
/**
* Structured-extraction configuration for an isolated agent run.
*
* The extraction pass always runs tools-off on its own timeout, fed from the
* run's tool-execution records — so a research generate that died on a
* provider error still extracts from the records instead of losing the run.
*/
export type IsolatedAgentExtraction = {
/** Local validator (lenient — may carry defaults/catch). Required. */
schema: z.ZodSchema;
/**
* Provider-attached wire schema (strict: no defaults/catch, which many
* providers reject in constrained decoding). Falls back to `schema`.
*/
wireSchema?: z.ZodSchema;
/**
* Human/model-readable JSON shape description used in the extraction
* prompt and in corrective retries alongside validation errors.
*/
shapeDoc?: string;
/**
* Normalizer applied to every recovery candidate before validation
* (e.g. wrap a bare top-level array into the expected envelope).
*/
coerce?: (candidate: unknown) => unknown;
/** Corrective re-ask attempts after ladder failure (default 2). */
maxRetries?: number;
/**
* Per-attempt extraction timeout in ms (default 60_000). Never carved out
* of the research budget.
*/
timeoutMs?: number;
/**
* Phase-level deadline bounding ALL extraction attempts (ms). Defaults to
* `(maxRetries + 1) × timeoutMs` — i.e. 180s at the defaults. Callers that
* derive an outer tool-execution ceiling from the leg budget must add this
* bound (plus the ~20s next-plan ask in leashed mode) on top of the
* research budget.
*/
totalTimeoutMs?: number;
};
/**
* Definition of an isolated sub-agent: a standard AgentDefinition plus
* optional structured extraction.
*/
export type IsolatedAgentDefinition = AgentDefinition & {
extraction?: IsolatedAgentExtraction;
};
/** Per-run overrides for an isolated agent run (internal-caller knobs). */
export type AgentRunOverrides = {
/** Wall-clock cap for the research pass (ms). */
turnTimeoutMs?: number;
/** Stall watchdog for the research pass (ms). */
stallTimeoutMs?: number;
/** Wrap-up lead for the research pass (ms). */
wrapupTimeLeadMs?: number;
/** Max agentic steps for the research pass. */
maxSteps?: number;
/** Max output tokens per model call. */
maxTokens?: number;
/** Model override (wins over the definition). */
model?: string;
/** Provider override (wins over the definition). */
provider?: string;
};
/** Leg budget for leashed mode. Setting either enables leashed mode. */
export type AgentLegOptions = {
/** Wall-clock budget for one leg (ms). */
budgetMs?: number;
/** Tool-call budget for one leg. */
budgetToolCalls?: number;
};
/**
* Mechanical waste-signature thresholds, checked per tool call. A tripped
* signature ends the leg early with `wasteSignals` populated.
*/
export type AgentWasteThresholds = {
/**
* Max times the same call hash (tool + normalized params) may be seen in
* one run before tripping (default 2 — the third identical call trips).
*/
duplicateCallLimit?: number;
/** Consecutive empty/zero-result calls before tripping (default 3). */
emptyResultStreakLimit?: number;
/** Consecutive error results before tripping (default 3). */
errorStreakLimit?: number;
/**
* Calls without a new distinct result payload before tripping
* (default 8).
*/
noNewResultsLimit?: number;
};
/** Lifecycle event types emitted by an isolated agent run. */
export type AgentRunEventType = "start" | "tool_call" | "tool_result" | "phase" | "wrapup" | "leg_end" | "waste" | "complete" | "error";
/**
* Lifecycle event for `AgentRunOptions.onEvent`. Fire-and-forget — listener
* errors never break the run.
*
* Note: `tool_call` and `tool_result` both fire AFTER the execution
* completes (they are driven by the capture record) — a long-running tool
* emits nothing until it returns. Treat them as accounting events, not
* live started/finished signals.
*/
export type AgentRunEvent = {
type: AgentRunEventType;
/** Run id (also the default tool-context sessionId). */
runId: string;
/** Agent definition id. */
agentId: string;
/** Epoch milliseconds. */
timestamp: number;
/** Current phase, on `phase` events. */
phase?: "research" | "extraction";
/** Tool name, on tool_call/tool_result events. */
toolName?: string;
/** Tool params, on tool_call events. */
params?: unknown;
/** Bounded result summary, on tool_result events. */
resultSummary?: string;
/** Whether the tool result was an error, on tool_result events. */
isError?: boolean;
/** Leg index, on leg_end events (leashed mode). */
legIndex?: number;
/** Tripped waste signatures, on waste events. */
wasteSignals?: string[];
/** Error message, on error events. */
error?: string;
/** Final status, on complete events. */
status?: AgentRunStatus;
};
/**
* Options for one isolated agent run (`NeuroLink.runIsolatedAgent`).
*/
export type AgentRunOptions = {
/**
* Parent cancellation. When provided it MUST be honored: an aborted parent
* stops research AND extraction cleanly, the outcome reports the run was
* cancelled, and no ghost workers survive.
*/
abortSignal?: AbortSignal;
/** Per-run overrides (internal-caller knobs). */
overrides?: AgentRunOverrides;
/**
* Set on the worker for EVERY tool call (merged into the worker's tool
* context), including a caller-supplied `sessionId`; the run id is used
* when no sessionId is supplied.
*/
toolContext?: Record<string, unknown>;
/** Lifecycle event stream. Fire-and-forget. */
onEvent?: (event: AgentRunEvent) => void;
/** Leg budget — enables leashed mode. */
leg?: AgentLegOptions;
/** TTL for a leashed handle in ms (default 600_000 = 10 min). */
handleTtlMs?: number;
/** Waste-signature thresholds (defaults apply when omitted). */
waste?: AgentWasteThresholds;
/**
* Bounds for the run's tool execution records. Raise `maxResultChars`
* when the CALLER verifies evidence from `toolExecutions` (raw result
* texts must be available up to the cap).
*/
capture?: {
maxResultChars?: number;
maxRecords?: number;
};
};
/** Terminal (or leg-boundary) status of an isolated agent run. */
export type AgentRunStatus = "completed" | "partial" | "in_progress" | "insufficient_data" | "error";
/** Per-leg accounting for leashed runs. */
export type AgentRunLegInfo = {
/** 0-based leg index. */
index: number;
/** Tool calls made this leg. */
toolCalls: number;
/** Wall-clock duration of this leg (ms). */
durationMs: number;
};
/** Cumulative budget accounting for leashed runs. */
export type AgentRunBudget = {
/** Total wall-clock spent across legs (ms). */
spentMs: number;
/** Remaining wall-clock vs the leg budget (ms; 0 when exhausted). */
remainingMs: number;
/** Total tool calls across legs. */
spentToolCalls: number;
};
/**
* Outcome of an isolated agent run.
*
* Delivery guarantee: a run with a non-empty execution record never returns
* an empty result — when extraction is unrecoverable, `data` carries a
* mechanical digest (which tools ran, ok/failed counts, bounded excerpts of
* successful payloads).
*/
export type AgentRunOutcome = {
status: AgentRunStatus;
/** Schema-valid extraction output when `extraction.schema` was given. */
data?: unknown;
/** Final research-pass text (the worker's own narrative). */
content?: string;
/** Why the research turn ended (honest — see GenerateStopReason). */
stopReason?: GenerateStopReason | string;
/**
* Real tool execution records (params/results/timing). Terminal outcomes
* carry the WHOLE run's records (all legs — what `data` was built from);
* `in_progress` legs carry this leg's records only.
*/
toolExecutions: ToolExecutionRecord[];
/** Aggregated token usage for the run (research + extraction). */
usage?: TokenUsage;
/** Wall-clock duration of this call (ms). */
durationMs: number;
/** Present when extraction fell back through the recovery ladder. */
extractionSource?: string;
/** Extraction/validation error summary when data is a mechanical digest. */
extractionError?: string;
/** Resume handle (status "in_progress" only). */
handle?: string;
/** This leg's accounting. */
leg?: AgentRunLegInfo;
/** One-line query→outcome summaries for this leg. */
delta?: string[];
/** The worker's own stated intent for the next leg. */
nextPlan?: string;
/** Tripped waste signatures, if any. */
wasteSignals?: string[];
/** Cumulative budget accounting. */
budget?: AgentRunBudget;
};
/** Where a recovered value came from, for observability and outcome fields. */
export type StructuredRecoverySource = "structured-data" | "raw-json" | "json-fence" | "brace-span" | "top-level-array";
/** One recovery candidate: source label + parsed (uncoerced) value. */
export type StructuredRecoveryCandidate = {
source: StructuredRecoverySource;
value: unknown;
};
/** Result of a structured recovery attempt. */
export type StructuredRecoveryResult = {
/** Schema-valid data, when any candidate survived validation. */
data?: unknown;
/** Which ladder rung produced the winning candidate. */
source?: StructuredRecoverySource;
/** Validation error summaries per failed candidate (for re-ask prompts). */
errors: string[];
};
/** Mechanical digest shape (delivery-guarantee fallback payload). */
export type AgentMechanicalDigest = {
kind: "mechanical-digest";
toolsRun: Record<string, {
calls: number;
ok: number;
failed: number;
}>;
excerpts: Array<{
toolName: string;
params: string;
resultText: string;
}>;
};
/**
* Options for `NeuroLink.registerAgentTool()` — wraps an isolated agent as a
* tool on the HOST instance so its existing generate() loop delegates
* without a second router generate.
*/
export type AgentToolRegistrationOptions = {
/** Tool name (default: the agent definition id). */
name?: string;
/**
* Max delegations to this tool per top-level generate() turn, counted in
* the loop itself. A refused call returns a recovery instruction, never a
* silent failure.
*/
maxDelegationsPerTurn?: number;
/**
* Max delegation depth (via tool context `agentDepth`). At the limit the
* tool is withheld from the request entirely.
*/
maxDepth?: number;
/**
* Process-wide concurrent delegation pool size. The pool is shared across
* all registered agent tools; the largest registered value wins (default
* 4) — registering raises the pool and can never lower it, so this is NOT
* a per-agent throttle. Nested delegations (agentDepth > 0) bypass the
* pool: the outer delegation already holds a slot, and queueing nested
* work behind a full pool would deadlock it.
*/
maxConcurrent?: number;
/** Queue timeout when the pool is saturated (ms, default 30_000). */
poolQueueTimeoutMs?: number;
/** Leashed-by-default leg budget for this tool. */
leg?: AgentLegOptions;
/** Handle TTL for leashed delegations (ms, default 600_000). */
handleTtlMs?: number;
/** Waste thresholds forwarded to each delegated run. */
waste?: AgentWasteThresholds;
/** Per-run overrides forwarded to each delegated run. */
overrides?: AgentRunOverrides;
};