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

246 lines (245 loc) • 10.4 kB
/** * Artifact Store Types (canonical location) * * Types for the MCP large-output artifact storage system. * When mcp.outputLimits.strategy = "externalize", oversized MCP tool outputs * are stored as artifacts and the model receives a compact surrogate instead. * * @module types/artifactTypes */ import type { RedisStorageConfig } from "./conversation.js"; /** Metadata recorded alongside a stored artifact. */ export type ArtifactMeta = { /** Tool name that produced the output. */ toolName: string; /** MCP server ID. */ serverId: string; /** Session that triggered the tool call (optional). */ sessionId?: string; /** Serialized byte size of the full payload. */ sizeBytes: number; /** Whether the payload is valid JSON or plain text. */ contentType: "json" | "text"; /** Unix epoch ms when the artifact was created. */ createdAt: number; /** * Human label for a host-banked artifact (e.g. "delegate:auth-review"). * Absent on artifacts written by the MCP output normalizer. */ label?: string; /** What kind of output was banked. Absent for MCP surrogates. */ kind?: BankedArtifactKind; }; /** Lightweight descriptor returned after a successful ArtifactStore.store(). */ export type ArtifactRef = { /** UUID v4 — stable identifier used in surrogate results and metadata. */ id: string; /** First N characters of the payload (for surrogate headers). */ preview: string; /** Full serialized byte size. */ sizeBytes: number; /** Stored metadata. */ meta: ArtifactMeta; }; /** * What a banked payload is, so previews and logs can say so without the * caller re-explaining itself. "other" is the escape hatch, not the default. */ export type BankedArtifactKind = "worker-report" | "command-output" | "stage-output" | "other"; /** How to bank one payload. Only `kind` and `label` are required. */ export type BankArtifactOptions = { /** What this payload is. */ kind: BankedArtifactKind; /** Short human label, e.g. "delegate:auth-review" — shown in logs. */ label: string; /** Session the payload belongs to, recorded on the artifact metadata. */ sessionId?: string; /** Payload shape; decides the on-disk extension. Default "text". */ contentType?: "json" | "text"; /** Preview length in characters. Default 1000, hard cap 4000. */ previewChars?: number; }; /** * What the conversation gets instead of the payload: an id, a bounded head * slice, and the exact call that reads the rest. The FULL payload is always on * disk — a preview is a pointer, never a replacement. */ export type BankedArtifactRef = { /** Id to pass to `retrieve_context({ artifactId })`. */ artifactId: string; label: string; kind: BankedArtifactKind; /** UTF-8 byte size of the complete payload. */ sizeBytes: number; /** Bounded head slice of the payload (characters, not bytes). */ preview: string; /** Literal read-back call, so the model never has to guess the tool. */ readBackHint: string; }; /** Character window for a paginated artifact read. */ export type ArtifactPageRequest = { /** Character offset to start at. Default 0. */ offset?: number; /** Maximum characters to return. Default: the rest of the payload. */ limit?: number; }; /** * One window of an artifact, as returned by `ArtifactStore.retrieveRange` or * by the shared reader when the store only supports whole-payload reads. * * Offsets and lengths are CHARACTERS (UTF-16 code units, the unit * `String.prototype.slice` and `retrieve_context`'s `offset` / `limit` use), * never bytes — so a model advancing `offset` by the characters it received * lands exactly where the previous window ended. */ export type ArtifactWindow = { /** The characters in `[offset, offset + content.length)`. */ content: string; /** Character offset this window starts at. */ offset: number; /** Total character length of the whole payload. */ totalLength: number; }; /** One hit from a literal search over an artifact. */ export type ArtifactSearchMatch = { /** Character offset of the match — pass it back as `offset` to read there. */ offset: number; /** 1-based line number the match sits on. */ line: number; /** Character offset the snippet starts at (≤ `offset`). */ snippetOffset: number; /** * Bounded context around the match. Bounded on purpose: an MCP artifact is * usually one compact JSON line, so "the matching line" would be the whole * payload. */ snippet: string; }; /** Result of a literal search over an artifact. */ export type ArtifactSearchResult = { /** Matches returned, in payload order. */ matches: ArtifactSearchMatch[]; /** `matches.length`. */ matchCount: number; /** Every match in the payload, including the ones not returned. */ totalMatches: number; /** True when `totalMatches > matchCount`. */ truncated: boolean; /** Character offset to pass as `offset` to search for the next matches. */ nextSearchOffset?: number; }; /** * Pluggable storage contract for externalized MCP tool outputs and banked * payloads. * * Shipped backends: `LocalTempArtifactStore` (filesystem, per-process index * with a cross-process sidecar) and `RedisArtifactStore` (TTL-expired, shared * across replicas, range reads). Pick one with `artifacts.storage` or the * `STORAGE_TYPE` environment variable, or inject any implementation via * `artifacts.store` / `setArtifactStore()`. * * Only `store`, `retrieve`, `delete`, `cleanup` and `generatePreview` are * required. `retrieveRange` and `close` are optional capabilities: NeuroLink * uses them when present and falls back cleanly when absent. */ export type ArtifactStore = { /** * Persist a payload and return a lightweight reference. * @param payload Serialized tool output (JSON string or plain text). * @param meta Descriptor without `createdAt` (assigned internally). */ store(payload: string, meta: Omit<ArtifactMeta, "createdAt">): Promise<ArtifactRef>; /** * Retrieve the full payload by artifact ID. * Returns `null` if the artifact is not found or has been cleaned up. */ retrieve(id: string): Promise<string | null>; /** * Retrieve one character window without materialising the whole payload. * * Optional. When present, `retrieve_context` and `readArtifact` call it for * every paged read instead of `retrieve()` + slice, so a backend with native * range reads (Redis `GETRANGE`, S3 `Range`) moves only the window. The * result carries `totalLength` so `hasMore` never needs the payload. * * `offset` and `limit` are characters. A backend that can only address * bytes must either know the payload is single-byte (ASCII) or fall back to * a full read and slice — it must never return a window that starts at the * wrong character. `limit` omitted means "to the end". * * Returns `null` if the artifact is not found or has expired. */ retrieveRange?(id: string, range: ArtifactPageRequest): Promise<ArtifactWindow | null>; /** Delete a single artifact. No-op if the ID does not exist. */ delete(id: string): Promise<void>; /** * Delete all artifacts older than `olderThanMs` milliseconds. * Returns the number of artifacts deleted. */ cleanup(olderThanMs: number): Promise<number>; /** Generate a short preview string from a serialized payload. */ generatePreview(payload: string): string; /** * Release whatever the store holds open (a pooled connection, a file * handle). Optional. NeuroLink calls it — from `shutdown()`, and when * `setArtifactStore()` replaces the store — only for stores it built * itself; a store you inject is yours to close. */ close?(): Promise<void>; }; /** * In-memory index row tracked by LocalTempArtifactStore. * Combines metadata with the on-disk path. */ export type IndexEntry = ArtifactMeta & { path: string; /** * Loaded from disk rather than stored by this process. Another process's * work: readable, but never expired by this process's `cleanup()`. */ rehydrated?: boolean; }; /** * Where artifacts live. Mirrors conversation memory's `STORAGE_TYPE`: * - "local" OS temp directory, per-process index with a cross-process * sidecar. Fine for one machine; artifacts do not survive a pod. * - "redis" Shared across replicas, expired by TTL, range reads via * `GETRANGE`. Uses the same connection pool as Redis conversation * memory. */ export type ArtifactStorageType = "local" | "redis"; /** * Artifact storage configuration (`new NeuroLink({ artifacts })`). * * Resolution order for the backend: `store` → `storage` → `STORAGE_TYPE` * environment variable → `"local"`. Resolution order for the Redis connection: * `redisConfig` → `conversationMemory.redisConfig` → `REDIS_URL` / `REDIS_HOST` * environment variables — so a deployment already running conversation memory * on Redis keeps its artifacts on the same Redis without new settings. */ export type ArtifactStorageConfig = { /** Backend to use. Default: `STORAGE_TYPE` env var, else `"local"`. */ storage?: ArtifactStorageType; /** * Redis connection for `storage: "redis"`. `keyPrefix` defaults to * `neurolink:artifact:` (NOT the conversation prefix). `ttl` is seconds, * must be positive, and defaults to 86400; zero or negative is replaced by * the default with a warning — artifacts in Redis always expire. * `userSessionsKeyPrefix` is ignored. */ redisConfig?: RedisStorageConfig; /** * A ready-made backend. Wins over `storage`. Use this for S3, a database, * or a wrapped store; `setArtifactStore()` does the same after construction. */ store?: ArtifactStore; }; /** * What `RedisArtifactStore` keeps beside each payload. `charLength` is what * makes range reads honest: when it equals `sizeBytes` the payload is pure * ASCII and a byte range IS a character range. */ export type RedisArtifactRecord = ArtifactMeta & { /** `payload.length` at store time — characters, not bytes. */ charLength: number; };