{"version":3,"file":"catalog.mjs","names":[],"sources":["../../../../../../../ai/src/skills/catalog.ts"],"sourcesContent":["import type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n  SkillCatalogEntry,\n  SkillRecord,\n} from \"./contracts/skill-record.type\";\nimport type { SkillsStoreContract } from \"./contracts/skills-store.contract\";\n\n// ============================================================\n// Optional embedder (OPTIONAL peer)\n// ============================================================\n//\n// The embedder is needed ONLY for `inject.select === \"semantic\"`. It is\n// passed explicitly via `inject.embedder` in the common case (consumers\n// reuse the one they built for `ai.memory()`). When a consumer relies on\n// an auto-resolved embedder instead, the canonical lazy-peer probe below\n// surfaces a curated install string at USE TIME (first semantic preload)\n// rather than a raw module-resolution stack trace. Catalog-only /\n// loadSkill-only usage never touches this path.\n\nlet isEmbedderPeerInstalled: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst EMBEDDER_INSTALL_INSTRUCTIONS = `\nSemantic skill pre-injection ({ inject: { select: \"semantic\" } }) needs an\nembedder. Pass one explicitly (reuse the one you built for ai.memory()):\n\n  skills({ inject: { select: \"semantic\", topK: 2, embedder } })\n\nor install an embedder provider:\n\n  npm install @warlock.js/ai-openai\n\nOr with your preferred package manager:\n\n  pnpm add @warlock.js/ai-openai\n  yarn add @warlock.js/ai-openai\n\nThen build one with \\`new OpenAIEmbedder(client, { name: \"text-embedding-3-small\" })\\`\nand pass it via \\`inject.embedder\\`.\n`.trim();\n\n/**\n * Probe for an installed embedder provider once, concurrency-safe. A bare\n * `catch` flips the flag to `false`; the curated install string surfaces\n * at use time. The provider's embedder needs a constructed SDK client, so\n * we cannot auto-build one — the probe only decides whether the curated\n * message should mention installing the package vs. just passing one in.\n */\nfunction probeEmbedderPeer(): Promise<void> {\n  if (isEmbedderPeerInstalled !== null) {\n    return Promise.resolve();\n  }\n\n  if (loadingPromise) {\n    return loadingPromise;\n  }\n\n  loadingPromise = (async () => {\n    try {\n      await import(\"@warlock.js/ai-openai\");\n      isEmbedderPeerInstalled = true;\n    } catch {\n      isEmbedderPeerInstalled = false;\n    }\n  })();\n\n  return loadingPromise;\n}\n\n/**\n * Resolve the embedder for semantic selection. The explicit\n * `inject.embedder` always wins. With none supplied, the lazy probe runs\n * and the curated install string is thrown at use time — a provider's\n * embedder requires a constructed client, so there is no safe auto-build.\n */\nasync function resolveEmbedder(explicit?: EmbedderContract): Promise<EmbedderContract> {\n  if (explicit) {\n    return explicit;\n  }\n\n  // Warm the peer probe non-blockingly (so a future explicit call can hint\n  // whether to install vs. just pass one in) but do NOT await it — a\n  // provider's embedder needs a constructed client, so there is no safe\n  // auto-build either way and the throw is immediate.\n  void probeEmbedderPeer();\n\n  throw new Error(EMBEDDER_INSTALL_INSTRUCTIONS);\n}\n\n/**\n * Merge every source's `list()` into one de-duplicated catalog. Sources\n * are merged in order; a LATER source wins on a name collision (explicit,\n * documented precedence). Candidates are already filtered by each store's\n * `list()`, so the merged catalog never carries an inert candidate.\n */\nexport async function buildCatalog(\n  stores: SkillsStoreContract[],\n  scope?: { tags?: string[] },\n): Promise<SkillCatalogEntry[]> {\n  const merged = new Map<string, SkillCatalogEntry>();\n\n  for (const store of stores) {\n    const entries = await store.list(scope);\n\n    for (const entry of entries) {\n      merged.set(entry.name, entry);\n    }\n  }\n\n  return [...merged.values()];\n}\n\n/**\n * Render the catalog as one line per skill — `name`, `version`,\n * `description` — matching the projection `scripts/generate-llms.mjs`\n * emits for `llms.txt` so the runtime catalog and the docs index read\n * identically. Returns an empty string when no skills are in scope so the\n * agent prepends nothing.\n */\nexport function renderCatalogPrompt(name: string, entries: SkillCatalogEntry[]): string {\n  if (entries.length === 0) {\n    return \"\";\n  }\n\n  const lines = entries.map(\n    (entry) => `- ${entry.name} (v${entry.version}): ${entry.description}`,\n  );\n\n  return [\n    `# Available skills — \"${name}\"`,\n    \"\",\n    \"You can load any of the following skills on demand with the `loadSkill` tool to pull its full instructions into context:\",\n    \"\",\n    ...lines,\n  ].join(\"\\n\");\n}\n\n/**\n * Load the full record for `name` across the merged sources, honoring the\n * later-source-wins precedence: the FIRST store (iterating in reverse) to\n * return a hit owns the name. A pinned `version` narrows the lookup.\n * Returns `undefined` when no source has the skill.\n */\nexport async function loadRecord(\n  stores: SkillsStoreContract[],\n  name: string,\n  version?: number,\n): Promise<SkillRecord | undefined> {\n  for (let index = stores.length - 1; index >= 0; index--) {\n    const record = await stores[index].load(name, version);\n\n    if (record) {\n      return record;\n    }\n  }\n\n  return undefined;\n}\n\n/**\n * Rank the in-scope catalog by cosine similarity to `input` and return the\n * full `SkillRecord`s for the top `topK` clearing `threshold`.\n *\n * Embeds `input` and every catalog `description` via the resolved\n * embedder (explicit `inject.embedder`, else the lazy provider), scores by\n * cosine similarity, sorts descending, applies the optional floor, slices\n * to `topK`, then loads those bodies. The embedder is the only optional\n * dependency this whole feature carries.\n */\nexport async function semanticPreselect(\n  stores: SkillsStoreContract[],\n  input: string,\n  topK: number,\n  options: { embedder?: EmbedderContract; threshold?: number; scope?: { tags?: string[] } } = {},\n): Promise<SkillRecord[]> {\n  const catalog = await buildCatalog(stores, options.scope);\n\n  if (catalog.length === 0 || topK <= 0) {\n    return [];\n  }\n\n  const embedder = await resolveEmbedder(options.embedder);\n\n  const { vectors } = await embedder.embedMany([\n    input,\n    ...catalog.map((entry) => entry.description),\n  ]);\n\n  const inputVector = vectors[0];\n  const threshold = options.threshold ?? 0;\n\n  const scored = catalog\n    .map((entry, index) => ({\n      entry,\n      score: cosineSimilarity(inputVector, vectors[index + 1]),\n    }))\n    .filter((candidate) => candidate.score >= threshold)\n    .sort((first, second) => second.score - first.score)\n    .slice(0, topK);\n\n  const records: SkillRecord[] = [];\n\n  for (const candidate of scored) {\n    const record = await loadRecord(stores, candidate.entry.name, candidate.entry.version);\n\n    if (record) {\n      records.push(record);\n    }\n  }\n\n  return records;\n}\n\n/** Cosine similarity of two equal-length vectors; `0` when either is degenerate. */\nfunction cosineSimilarity(a: number[], b: number[]): number {\n  let dot = 0;\n  let normA = 0;\n  let normB = 0;\n\n  for (let index = 0; index < a.length; index++) {\n    dot += a[index] * b[index];\n    normA += a[index] * a[index];\n    normB += b[index] * b[index];\n  }\n\n  if (normA === 0 || normB === 0) {\n    return 0;\n  }\n\n  return dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n"],"mappings":";AAmBA,IAAI,0BAA0C;AAC9C,IAAI;AAEJ,MAAM,gCAAgC;;;;;;;;;;;;;;;;;EAiBpC,KAAK;;;;;;;;AASP,SAAS,oBAAmC;CAC1C,IAAI,4BAA4B,MAC9B,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,MAAM,OAAO;GACb,0BAA0B;EAC5B,QAAQ;GACN,0BAA0B;EAC5B;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;AAQA,eAAe,gBAAgB,UAAwD;CACrF,IAAI,UACF,OAAO;CAOT,AAAK,kBAAkB;CAEvB,MAAM,IAAI,MAAM,6BAA6B;AAC/C;;;;;;;AAQA,eAAsB,aACpB,QACA,OAC8B;CAC9B,MAAM,yBAAS,IAAI,IAA+B;CAElD,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,UAAU,MAAM,MAAM,KAAK,KAAK;EAEtC,KAAK,MAAM,SAAS,SAClB,OAAO,IAAI,MAAM,MAAM,KAAK;CAEhC;CAEA,OAAO,CAAC,GAAG,OAAO,OAAO,CAAC;AAC5B;;;;;;;;AASA,SAAgB,oBAAoB,MAAc,SAAsC;CACtF,IAAI,QAAQ,WAAW,GACrB,OAAO;CAGT,MAAM,QAAQ,QAAQ,KACnB,UAAU,KAAK,MAAM,KAAK,KAAK,MAAM,QAAQ,KAAK,MAAM,aAC3D;CAEA,OAAO;EACL,yBAAyB,KAAK;EAC9B;EACA;EACA;EACA,GAAG;CACL,CAAC,CAAC,KAAK,IAAI;AACb;;;;;;;AAQA,eAAsB,WACpB,QACA,MACA,SACkC;CAClC,KAAK,IAAI,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG,SAAS;EACvD,MAAM,SAAS,MAAM,OAAO,MAAM,CAAC,KAAK,MAAM,OAAO;EAErD,IAAI,QACF,OAAO;CAEX;AAGF;;;;;;;;;;;AAYA,eAAsB,kBACpB,QACA,OACA,MACA,UAA4F,CAAC,GACrE;CACxB,MAAM,UAAU,MAAM,aAAa,QAAQ,QAAQ,KAAK;CAExD,IAAI,QAAQ,WAAW,KAAK,QAAQ,GAClC,OAAO,CAAC;CAKV,MAAM,EAAE,YAAY,OAAM,MAFH,gBAAgB,QAAQ,QAAQ,EAErB,CAAC,UAAU,CAC3C,OACA,GAAG,QAAQ,KAAK,UAAU,MAAM,WAAW,CAC7C,CAAC;CAED,MAAM,cAAc,QAAQ;CAC5B,MAAM,YAAY,QAAQ,aAAa;CAEvC,MAAM,SAAS,QACZ,KAAK,OAAO,WAAW;EACtB;EACA,OAAO,iBAAiB,aAAa,QAAQ,QAAQ,EAAE;CACzD,EAAE,CAAC,CACF,QAAQ,cAAc,UAAU,SAAS,SAAS,CAAC,CACnD,MAAM,OAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,CAAC,CACnD,MAAM,GAAG,IAAI;CAEhB,MAAM,UAAyB,CAAC;CAEhC,KAAK,MAAM,aAAa,QAAQ;EAC9B,MAAM,SAAS,MAAM,WAAW,QAAQ,UAAU,MAAM,MAAM,UAAU,MAAM,OAAO;EAErF,IAAI,QACF,QAAQ,KAAK,MAAM;CAEvB;CAEA,OAAO;AACT;;AAGA,SAAS,iBAAiB,GAAa,GAAqB;CAC1D,IAAI,MAAM;CACV,IAAI,QAAQ;CACZ,IAAI,QAAQ;CAEZ,KAAK,IAAI,QAAQ,GAAG,QAAQ,EAAE,QAAQ,SAAS;EAC7C,OAAO,EAAE,SAAS,EAAE;EACpB,SAAS,EAAE,SAAS,EAAE;EACtB,SAAS,EAAE,SAAS,EAAE;CACxB;CAEA,IAAI,UAAU,KAAK,UAAU,GAC3B,OAAO;CAGT,OAAO,OAAO,KAAK,KAAK,KAAK,IAAI,KAAK,KAAK,KAAK;AAClD"}