UNPKG

@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
/** * 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; };