@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
305 lines (304 loc) • 11.5 kB
JavaScript
/**
* S3-backed skill store.
*
* Layout (curator-compatible):
* <prefix>skills/<id>.json — one JSON-serialized SkillDefinition per object
* <prefix>index.json — { lastUpdated, skills: SkillIndexItem[] }
*
* The index document is upserted on every write and rebuilt from a bucket
* listing when missing or unparsable (self-healing). Concurrent writers can
* race the index read-modify-write; the rebuild path recovers from any
* resulting drift, matching curator's production behavior.
*
* @aws-sdk/client-s3 is an optional peer — loaded lazily with the same
* createRequire pattern as memory's @juspay/hippocampus so importing
* NeuroLink core never requires the AWS SDK. Tests (and hosts with
* pre-configured clients) inject a SkillS3ObjectOps implementation instead.
*/
import { createRequire } from "node:module";
import { logger } from "../utils/logger.js";
import { isSafeSkillResourcePath, toSkillIndexItem } from "./skillMatcher.js";
const lazyRequire = createRequire(import.meta.url);
let cachedS3Module;
function loadS3Module() {
if (cachedS3Module !== undefined) {
return cachedS3Module;
}
try {
cachedS3Module = lazyRequire("@aws-sdk/client-s3");
return cachedS3Module;
}
catch (error) {
cachedS3Module = null;
logger.debug("[SkillStoreS3] @aws-sdk/client-s3 is not installed; S3 skill storage unavailable.", { error: error instanceof Error ? error.message : String(error) });
return null;
}
}
/** Build default object ops from @aws-sdk/client-s3. Throws when absent. */
function createDefaultOps(config) {
const mod = loadS3Module();
if (!mod) {
throw new Error("Skills S3 storage requires @aws-sdk/client-s3. Run `pnpm add @aws-sdk/client-s3` " +
"(or your package manager equivalent), or supply a custom store instead.");
}
const client = new mod.S3Client({
...(config.region ? { region: config.region } : {}),
...(config.endpoint ? { endpoint: config.endpoint } : {}),
...(config.forcePathStyle !== undefined
? { forcePathStyle: config.forcePathStyle }
: {}),
...(config.credentials ? { credentials: config.credentials } : {}),
});
const bucket = config.bucket;
return {
async getObject(key) {
try {
const response = (await client.send(new mod.GetObjectCommand({ Bucket: bucket, Key: key })));
return (await response.Body?.transformToString()) ?? null;
}
catch (error) {
const name = error.name;
if (name === "NoSuchKey" || name === "NotFound") {
return null;
}
throw error;
}
},
async putObject(key, body) {
await client.send(new mod.PutObjectCommand({
Bucket: bucket,
Key: key,
Body: body,
ContentType: "application/json",
}));
},
async deleteObject(key) {
await client.send(new mod.DeleteObjectCommand({ Bucket: bucket, Key: key }));
},
async listKeys(prefix) {
const keys = [];
let continuationToken;
do {
const response = (await client.send(new mod.ListObjectsV2Command({
Bucket: bucket,
Prefix: prefix,
...(continuationToken
? { ContinuationToken: continuationToken }
: {}),
})));
for (const item of response.Contents ?? []) {
if (item.Key) {
keys.push(item.Key);
}
}
continuationToken = response.IsTruncated
? response.NextContinuationToken
: undefined;
} while (continuationToken);
return keys;
},
async getObjectConditional(key, etag) {
try {
const response = (await client.send(new mod.GetObjectCommand({
Bucket: bucket,
Key: key,
...(etag ? { IfNoneMatch: etag } : {}),
})));
const body = (await response.Body?.transformToString()) ?? null;
return {
body,
...(response.ETag ? { etag: response.ETag } : {}),
};
}
catch (error) {
const name = error.name;
const status = error
.$metadata?.httpStatusCode;
if (status === 304 || name === "304" || name === "NotModified") {
return { body: null, notModified: true };
}
if (name === "NoSuchKey" || name === "NotFound") {
return { body: null };
}
throw error;
}
},
};
}
export class S3SkillStore {
config;
injectedOps;
prefix;
ops = null;
/** Last parsed index + its ETag, revalidated with If-None-Match reads. */
cachedIndexDoc = null;
cachedIndexEtag;
constructor(config,
/** Test/host seam — omit to build ops from @aws-sdk/client-s3 lazily. */
injectedOps) {
this.config = config;
this.injectedOps = injectedOps;
const rawPrefix = config.prefix ?? "neurolink-skills/";
this.prefix =
rawPrefix === "" || rawPrefix.endsWith("/") ? rawPrefix : `${rawPrefix}/`;
}
invalidate() {
this.cachedIndexDoc = null;
this.cachedIndexEtag = undefined;
}
getOps() {
if (!this.ops) {
this.ops = this.injectedOps ?? createDefaultOps(this.config);
}
return this.ops;
}
skillKey(id) {
return `${this.prefix}skills/${id}.json`;
}
indexKey() {
return `${this.prefix}index.json`;
}
/** Resources live beside the skill object: <prefix>skills/<id>/<path>. */
resourceKey(id, resourcePath) {
return `${this.prefix}skills/${id}/${resourcePath}`;
}
async getResource(id, resourcePath) {
if (!isSafeSkillResourcePath(resourcePath)) {
return null;
}
return this.getOps().getObject(this.resourceKey(id, resourcePath));
}
async get(id) {
const body = await this.getOps().getObject(this.skillKey(id));
if (!body) {
return null;
}
try {
return JSON.parse(body);
}
catch (error) {
logger.warn("[SkillStoreS3] Failed to parse skill object", {
id,
error: error instanceof Error ? error.message : String(error),
});
return null;
}
}
async put(skill) {
const ops = this.getOps();
await ops.putObject(this.skillKey(skill.id), JSON.stringify(skill, null, 2));
const index = await this.readOrRebuildIndex();
const entry = toSkillIndexItem(skill);
const position = index.skills.findIndex((s) => s.id === skill.id);
if (position >= 0) {
index.skills[position] = entry;
}
else {
index.skills.push(entry);
}
await this.writeIndex(index);
}
async delete(id) {
const ops = this.getOps();
await ops.deleteObject(this.skillKey(id));
const index = await this.readOrRebuildIndex();
index.skills = index.skills.filter((s) => s.id !== id);
await this.writeIndex(index);
}
async index() {
const index = await this.readOrRebuildIndex();
return index.skills;
}
async writeIndex(index) {
index.lastUpdated = new Date().toISOString();
await this.getOps().putObject(this.indexKey(), JSON.stringify(index, null, 2));
// The write changed the object's ETag; keep the doc, revalidate next read.
this.cachedIndexDoc = index;
this.cachedIndexEtag = undefined;
}
/**
* Raw index.json read. Returns the body (with its ETag when available),
* null body when the object is absent, or notModified when a
* conditional read reported the cached copy unchanged.
*/
async readIndexObject(ops) {
if (ops.getObjectConditional) {
return ops.getObjectConditional(this.indexKey(), this.cachedIndexDoc ? this.cachedIndexEtag : undefined);
}
return { body: await ops.getObject(this.indexKey()) };
}
/**
* Read index.json (ETag-revalidated when the ops support conditional
* reads — an unchanged index costs a 304, not a download); when missing
* or unparsable, rebuild it from a listing of the skills/ prefix and
* persist the rebuilt document (self-heal).
*/
async readOrRebuildIndex() {
const ops = this.getOps();
const read = await this.readIndexObject(ops);
if (read.notModified && this.cachedIndexDoc) {
return this.cachedIndexDoc;
}
if (read.body) {
try {
const parsed = JSON.parse(read.body);
if (Array.isArray(parsed.skills)) {
this.cachedIndexDoc = parsed;
// Cache the ETag only for a body that parsed: pairing a corrupt
// object's ETag with the previous good doc would make every
// later conditional read 304 into permanently stale data.
this.cachedIndexEtag = read.etag;
return parsed;
}
}
catch (error) {
logger.warn("[SkillStoreS3] index.json unparsable — rebuilding", {
error: error instanceof Error ? error.message : String(error),
});
}
this.cachedIndexEtag = undefined;
}
const skillsPrefix = `${this.prefix}skills/`;
const keys = await ops.listKeys(skillsPrefix);
const skills = [];
for (const key of keys) {
if (!key.endsWith(".json")) {
continue;
}
const body = await ops.getObject(key);
if (!body) {
continue;
}
try {
skills.push(toSkillIndexItem(JSON.parse(body)));
}
catch {
logger.warn("[SkillStoreS3] Skipping unparsable skill during rebuild", {
key,
});
}
}
const rebuilt = {
lastUpdated: new Date().toISOString(),
skills,
};
// Cache the rebuilt doc even when persistence below fails, so reads
// don't fall back to a stale pre-rebuild copy.
this.cachedIndexDoc = rebuilt;
this.cachedIndexEtag = undefined;
try {
await this.writeIndex(rebuilt);
logger.info("[SkillStoreS3] Rebuilt skills index", {
count: skills.length,
});
}
catch (error) {
// Index persistence is best-effort; the rebuilt in-memory copy is
// still returned so reads keep working.
logger.warn("[SkillStoreS3] Failed to persist rebuilt index", {
error: error instanceof Error ? error.message : String(error),
});
}
return rebuilt;
}
}