@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
170 lines (169 loc) • 7.25 kB
JavaScript
/**
* Pure index-filtering + prompt-index formatting helpers.
*
* Matching semantics are ported from curator's battle-tested
* `searchSkillsFromIndex`: case-insensitive substring match on
* name/displayName/description, optional tag filter on top, scope filter
* that always admits global skills, and active-only visibility.
*/
/** Strip instructions from a full definition to form an index entry. */
export function toSkillIndexItem(skill) {
const { instructions: _instructions, ...indexItem } = skill;
return indexItem;
}
/**
* Sort index entries by name (byte order, locale-independent) so every
* listing renders byte-identically across calls and store backends.
* Store enumeration order (Redis SCAN, fs.readdir, S3 listings) is not
* stable — an unsorted listing would shuffle between calls and invalidate
* provider prompt caches. Returns a new array.
*/
export function sortSkillIndex(items) {
return [...items].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
}
/** Filter index entries by query/tag/scope. Active skills only. */
export function filterSkillIndex(items, query) {
const lowerQuery = query.query?.toLowerCase();
const lowerTag = query.tag?.toLowerCase();
const matched = items.filter((item) => {
if ((item.status ?? "active") !== "active") {
return false;
}
// Scope filter: global skills always pass. Scoped skills require a
// scopeId that matches; when the caller supplies no scopeId we fail
// closed and exclude them, so a shared multi-tenant instance that forgets
// to pass a per-call scopeId never leaks one tenant's scoped skills to
// everyone (#1139). Callers that legitimately want a tenant's scoped
// skills must pass that tenant's scopeId.
if (item.scope === "scoped") {
if (!query.scopeId || !(item.scopeIds ?? []).includes(query.scopeId)) {
return false;
}
}
if (lowerQuery) {
const nameMatch = item.name.toLowerCase().includes(lowerQuery) ||
(item.displayName ?? "").toLowerCase().includes(lowerQuery) ||
item.description.toLowerCase().includes(lowerQuery);
if (!nameMatch) {
return false;
}
}
if (lowerTag) {
const tagMatch = (item.tags ?? []).some((t) => t.toLowerCase().includes(lowerTag));
if (!tagMatch) {
return false;
}
}
return true;
});
return query.limit !== undefined ? matched.slice(0, query.limit) : matched;
}
/**
* Reject resource paths that could escape a skill's namespace: absolute
* paths, traversal segments, and empty segments. The manager validates
* before reaching any store — this is defense in depth for stores whose
* keys are built by concatenation (S3, Redis) or path joining.
*/
export function isSafeSkillResourcePath(resourcePath) {
const normalized = resourcePath.replace(/\\/g, "/");
return (normalized.length > 0 &&
!normalized.startsWith("/") &&
!normalized.split("/").some((segment) => segment === ".." || segment === ""));
}
/**
* Whether a skill is visible from the calling scope. Fails closed, matching
* `filterSkillIndex`: a scoped skill is hidden unless the caller supplies a
* `scopeId` that the skill explicitly lists. Omitting `scopeId` therefore
* yields global skills only — it must never expose a scoped skill by name
* through `use_skill` / `read_skill_resource` (#1139).
*/
export function isSkillVisibleInScope(skill, scopeId) {
if (skill.scope !== "scoped") {
return true;
}
if (!scopeId) {
return false;
}
return (skill.scopeIds ?? []).includes(scopeId);
}
/** Trim a description to its first sentence (or the whole text when unsplittable). */
function firstSentence(description) {
const match = description.match(/^[^.!?]*[.!?]/);
return match ? match[0].trim() : description;
}
/**
* Render the `<available_skills>` block embedded in the use_skill tool
* description ("tool" discovery mode). One line per skill — name,
* description, tags — instructions never appear here.
*
* Budget handling is uniform and stateless: over budget, every
* description drops to its first sentence, then to a hard 80-char cap.
* The render is a pure function of the (sorted) index, so the tool
* description stays byte-identical across calls — a prerequisite for
* provider prompt caching. Names are never dropped.
*/
export function renderSkillListing(items, budgetChars) {
if (items.length === 0) {
return null;
}
const render = (describe) => {
const lines = items.map((item) => {
const tags = item.tags && item.tags.length > 0
? ` [tags: ${item.tags.join(", ")}]`
: "";
return `- ${item.name}: ${describe(item)}${tags}`;
});
return ["<available_skills>", ...lines, "</available_skills>"].join("\n");
};
const full = render((item) => item.description);
if (full.length <= budgetChars) {
return full;
}
const sentences = render((item) => firstSentence(item.description));
if (sentences.length <= budgetChars) {
return sentences;
}
const HARD_CAP = 80;
// Floor render — returned even if it still exceeds the budget.
return render((item) => {
const sentence = firstSentence(item.description);
return sentence.length > HARD_CAP
? `${sentence.slice(0, HARD_CAP - 1)}…`
: sentence;
});
}
/**
* Render the compact skills index appended to the system prompt
* ("system-prompt" discovery mode). Names + descriptions only —
* instructions are never included; the model loads them via use_skill.
* Returns null when nothing is visible so callers can skip injection.
*/
export function formatSkillsPromptIndex(items, maxItems) {
// Nothing visible, or the index is explicitly disabled (maxItems <= 0):
// skip injection entirely rather than emit a header + "N more skills exist"
// note with zero skill lines (#1139).
if (items.length === 0 || maxItems <= 0) {
return null;
}
const visible = items.slice(0, maxItems);
const lines = visible.map((item) => {
const label = item.displayName
? `${item.name} (${item.displayName})`
: item.name;
const tags = item.tags && item.tags.length > 0
? ` [tags: ${item.tags.join(", ")}]`
: "";
return `- ${label}: ${item.description}${tags}`;
});
const truncationNote = items.length > visible.length
? `\n(${items.length - visible.length} more skills exist — use list_skills to discover them.)`
: "";
return ([
"## Available Skills",
"The following team-defined skills (SOPs, playbooks, workflows) are available.",
"Before answering from general knowledge, check whether one applies to the user's request.",
"To use a skill, call the use_skill tool with its name to load the full instructions, then follow them exactly.",
"",
...lines,
].join("\n") + truncationNote);
}