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