@agenticmail/core 0.9.23 → 0.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,6 +4,19 @@ import {
4
4
  isInternalEmail,
5
5
  scoreEmail
6
6
  } from "./chunk-TIAKW5DC.js";
7
+ import {
8
+ MemorySearchIndex,
9
+ invalidateSkillCache,
10
+ listSkills,
11
+ loadSkill,
12
+ renderSkillAsPrompt,
13
+ saveUserSkill,
14
+ searchSkills,
15
+ stem,
16
+ tokenize,
17
+ userSkillsDir,
18
+ validateSkill
19
+ } from "./chunk-J6JINNJ3.js";
7
20
  import {
8
21
  __require
9
22
  } from "./chunk-3RG5ZIWI.js";
@@ -8749,12 +8762,46 @@ var SEARCH_EMAIL_TOOL = {
8749
8762
  additionalProperties: false
8750
8763
  }
8751
8764
  };
8765
+ var SEARCH_SKILLS_TOOL = {
8766
+ type: "function",
8767
+ name: "search_skills",
8768
+ description: "Search your skill library for a playbook that fits the situation you just hit on this call (billing dispute, debt collector tactics, reservation deadlock, etc). Returns ranked summaries \u2014 pick the best match and pass its id to load_skill. Fast.",
8769
+ parameters: {
8770
+ type: "object",
8771
+ properties: {
8772
+ query: {
8773
+ type: "string",
8774
+ description: 'Plain-language description of the situation, e.g. "rep insists on a commitment date", "the restaurant is fully booked", "I need to dispute a recurring charge after cancellation".'
8775
+ }
8776
+ },
8777
+ required: ["query"],
8778
+ additionalProperties: false
8779
+ }
8780
+ };
8781
+ var LOAD_SKILL_TOOL = {
8782
+ type: "function",
8783
+ name: "load_skill",
8784
+ description: 'Load a skill playbook by id into your context for the rest of this call. The playbook (principles, scripted phrases, ordered tactics, hard boundaries, exit strategy) grounds your next turns. Always call search_skills first to find the right id. Before calling, say "hold on one moment" \u2014 loading is briefer than `ask_operator` but takes a beat.',
8785
+ parameters: {
8786
+ type: "object",
8787
+ properties: {
8788
+ id: {
8789
+ type: "string",
8790
+ description: 'Skill id (lowercase-hyphenated), e.g. "negotiate-bill-reduction". Get it from search_skills.'
8791
+ }
8792
+ },
8793
+ required: ["id"],
8794
+ additionalProperties: false
8795
+ }
8796
+ };
8752
8797
  var REALTIME_TOOL_DEFINITIONS = {
8753
8798
  ask_operator: ASK_OPERATOR_TOOL,
8754
8799
  web_search: WEB_SEARCH_TOOL,
8755
8800
  recall_memory: RECALL_MEMORY_TOOL,
8756
8801
  get_datetime: GET_DATETIME_TOOL,
8757
- search_email: SEARCH_EMAIL_TOOL
8802
+ search_email: SEARCH_EMAIL_TOOL,
8803
+ search_skills: SEARCH_SKILLS_TOOL,
8804
+ load_skill: LOAD_SKILL_TOOL
8758
8805
  };
8759
8806
  function buildRealtimeToolGuidance(tools) {
8760
8807
  if (tools.length === 0) return "";
@@ -8773,6 +8820,16 @@ function buildRealtimeToolGuidance(tools) {
8773
8820
  'The lookup tools (web_search, recall_memory, get_datetime, search_email) return in seconds \u2014 a brief "one moment" is plenty; no long hold is needed for these.'
8774
8821
  );
8775
8822
  }
8823
+ if (names.has("search_skills") && names.has("load_skill")) {
8824
+ lines.push(
8825
+ `Your SKILL LIBRARY contains playbooks for specific real-world phone situations \u2014 bill negotiation, debt-collector handling, restaurant booking, dispute filing, etc. Each playbook is a complete set of principles, scripted phrases, ordered tactics, boundaries, and exit strategy for that one situation. When you find yourself on the call without a clear next move \u2014 the rep brought up something you do not know how to handle, the conversation reached a stage that needs a specific tactic \u2014 load a skill instead of improvising:
8826
+ 1. Tell the caller you need a moment: "Hold on one moment \u2014 let me check something."
8827
+ 2. Call search_skills with a one-line description of the situation.
8828
+ 3. Call load_skill with the id of the best match.
8829
+ 4. Resume the call grounded in the playbook the load returned. Follow the playbook's tactic order, use its scripted phrases (paraphrased to match your voice), respect its hard boundaries, watch for its success / failure signals.
8830
+ A skill's rendered playbook is now part of your instructions for the rest of the call. You can load a second skill if a new situation comes up \u2014 but the model keeps a max of two loaded; a third load drops the oldest. Pick skills deliberately.`
8831
+ );
8832
+ }
8776
8833
  return lines.join("\n");
8777
8834
  }
8778
8835
  function toolErrorText(err) {
@@ -8958,6 +9015,7 @@ var REALTIME_AUDIO_SAMPLE_RATE = 24e3;
8958
9015
  var REALTIME_MAX_AUDIO_FRAME_BASE64 = 256 * 1024;
8959
9016
  var MAX_PENDING_AUDIO_FRAMES = 200;
8960
9017
  var REALTIME_TOOL_CALL_TIMEOUT_MS = 6 * 6e4;
9018
+ var MAX_LOADED_SKILLS = 2;
8961
9019
  var MAX_IN_FLIGHT_TOOL_CALLS = 8;
8962
9020
  var DEFAULT_PERSONA = "You are a helpful, professional voice assistant making a phone call on behalf of your operator. Speak naturally and concisely, the way a person would on a real call. Listen carefully, do not talk over the other party, and keep each turn short. Never invent facts; if you do not know something, say so. Do not reveal that you are an AI unless you are asked directly.";
8963
9021
  function buildRealtimeInstructions(opts) {
@@ -9049,6 +9107,26 @@ var RealtimeVoiceBridge = class {
9049
9107
  toolCallNames = /* @__PURE__ */ new Map();
9050
9108
  /** `call_id`s whose tool call is currently executing. */
9051
9109
  inFlightToolCalls = /* @__PURE__ */ new Set();
9110
+ /**
9111
+ * Mid-call skills loaded into the session so far, FIFO. Earliest at
9112
+ * index 0; cap at {@link MAX_LOADED_SKILLS}. When a (cap+1)th skill
9113
+ * is loaded the oldest one drops out — the model can't usefully
9114
+ * hold five playbooks in working memory at once, so we keep the
9115
+ * working set narrow on purpose.
9116
+ */
9117
+ loadedSkills = [];
9118
+ /**
9119
+ * The original `instructions` string from the session.update sent at
9120
+ * open. We keep a private copy because every mid-call skill load
9121
+ * issues a fresh `session.update` whose `instructions` is built as:
9122
+ *
9123
+ * baseInstructions + "\n\n" + renderedSkill1 + "\n\n" + renderedSkill2 …
9124
+ *
9125
+ * Without this snapshot, successive loads would compound — the second
9126
+ * load would see "base + skill1" as the base and append skill2 to
9127
+ * THAT, eventually drifting unboundedly.
9128
+ */
9129
+ baseInstructions = "";
9052
9130
  constructor(opts) {
9053
9131
  const carrier = opts.carrier ?? opts.elks;
9054
9132
  if (!carrier) {
@@ -9085,6 +9163,10 @@ var RealtimeVoiceBridge = class {
9085
9163
  handleOpenAIOpen() {
9086
9164
  if (this.ended || this.openaiReady) return;
9087
9165
  this.openaiReady = true;
9166
+ const sess = this.sessionConfig?.session;
9167
+ if (sess && typeof sess.instructions === "string") {
9168
+ this.baseInstructions = sess.instructions;
9169
+ }
9088
9170
  this.safeSend(this.openai, this.sessionConfig);
9089
9171
  this.safeSend(this.openai, { type: "response.create" });
9090
9172
  for (const audio of this.pendingAudio.splice(0)) {
@@ -9095,6 +9177,74 @@ var RealtimeVoiceBridge = class {
9095
9177
  handleOpenAIClose() {
9096
9178
  this.end("openai-closed");
9097
9179
  }
9180
+ /**
9181
+ * Load a skill playbook into the live OpenAI Realtime session for
9182
+ * the rest of the call.
9183
+ *
9184
+ * Mechanics:
9185
+ * 1. Resolve the skill JSON via the skills registry (file on disk).
9186
+ * 2. Append the rendered skill text to the agent's working
9187
+ * instructions and re-send a `session.update` carrying ONLY
9188
+ * the new `instructions` field. The OpenAI Realtime API
9189
+ * supports partial session.update — we don't have to re-send
9190
+ * audio config, tools, voice, etc.
9191
+ * 3. Track which skills are loaded so we (a) FIFO-evict the
9192
+ * oldest when the cap is hit and (b) include every still-
9193
+ * loaded skill in the next composed instructions.
9194
+ * 4. Emit a transcript marker so the mission record shows the
9195
+ * adaptation ("[skill loaded: Negotiate a Bill Reduction
9196
+ * v1.0.0]"). Useful for post-call review and for the build
9197
+ * farm's telemetry on which skills actually got reached for.
9198
+ *
9199
+ * Returns an object the {@link load_skill} tool handler can serialise
9200
+ * back to the model: `ok: true` plus the skill name + version on
9201
+ * success, `ok: false` plus a short reason on failure (unknown id,
9202
+ * call ended, registry I/O error). Never throws — a buggy registry
9203
+ * or a missing file must not crash the bridge mid-call.
9204
+ *
9205
+ * Phase 2 of the skill library (`docs/skill-library-plan.md`).
9206
+ */
9207
+ async loadSkillIntoSession(skillId) {
9208
+ if (this.ended) return { ok: false, message: "Call has already ended; cannot load a skill now." };
9209
+ if (!this.openaiReady) return { ok: false, message: "Session is not ready yet; try again in a moment." };
9210
+ if (this.loadedSkills.some((s) => s.id === skillId)) {
9211
+ const existing = this.loadedSkills.find((s) => s.id === skillId);
9212
+ return { ok: true, message: `Skill "${skillId}" is already loaded.`, name: skillId, version: existing.version };
9213
+ }
9214
+ let loadSkill2;
9215
+ let renderSkillAsPrompt2;
9216
+ try {
9217
+ ({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await import("./skills-RE3S767B.js"));
9218
+ } catch (err) {
9219
+ return { ok: false, message: `Skill registry unavailable: ${errorText(err)}` };
9220
+ }
9221
+ const skill = loadSkill2(skillId);
9222
+ if (!skill) {
9223
+ return { ok: false, message: `No skill found with id "${skillId}". Call search_skills first to find the right id.` };
9224
+ }
9225
+ const rendered = renderSkillAsPrompt2(skill);
9226
+ while (this.loadedSkills.length >= MAX_LOADED_SKILLS) {
9227
+ const dropped = this.loadedSkills.shift();
9228
+ if (dropped) {
9229
+ this.emitTranscript("system", `[skill unloaded for working-memory limit: ${dropped.id} v${dropped.version}]`);
9230
+ }
9231
+ }
9232
+ this.loadedSkills.push({ id: skill.id, version: skill.version, renderedPrompt: rendered });
9233
+ const composed = [
9234
+ this.baseInstructions,
9235
+ ...this.loadedSkills.map((s) => s.renderedPrompt)
9236
+ ].filter((s) => s && s.length > 0).join("\n\n");
9237
+ this.safeSend(this.openai, {
9238
+ type: "session.update",
9239
+ session: { instructions: composed }
9240
+ });
9241
+ this.emitTranscript("system", `[skill loaded: ${skill.name} v${skill.version}]`);
9242
+ return { ok: true, message: `Loaded skill: ${skill.name} (v${skill.version})`, name: skill.name, version: skill.version };
9243
+ }
9244
+ /** The list of skills currently loaded into the session (FIFO-ordered). */
9245
+ get loadedSkillIds() {
9246
+ return this.loadedSkills.map((s) => s.id);
9247
+ }
9098
9248
  /** Call when the OpenAI socket errors. */
9099
9249
  handleOpenAIError(err) {
9100
9250
  this.emitTranscript("system", `OpenAI Realtime error: ${errorText(err)}`);
@@ -12998,400 +13148,6 @@ function parse(raw) {
12998
13148
 
12999
13149
  // src/memory/manager.ts
13000
13150
  import { randomUUID as randomUUID3 } from "crypto";
13001
-
13002
- // src/memory/text-search.ts
13003
- var BM25_K1 = 1.2;
13004
- var BM25_B = 0.75;
13005
- var FIELD_WEIGHT_TITLE = 3;
13006
- var FIELD_WEIGHT_TAGS = 2;
13007
- var FIELD_WEIGHT_CONTENT = 1;
13008
- var PREFIX_MATCH_PENALTY = 0.7;
13009
- var STOP_WORDS = /* @__PURE__ */ new Set([
13010
- "a",
13011
- "about",
13012
- "above",
13013
- "after",
13014
- "again",
13015
- "against",
13016
- "all",
13017
- "am",
13018
- "an",
13019
- "and",
13020
- "any",
13021
- "are",
13022
- "as",
13023
- "at",
13024
- "be",
13025
- "because",
13026
- "been",
13027
- "before",
13028
- "being",
13029
- "below",
13030
- "between",
13031
- "both",
13032
- "but",
13033
- "by",
13034
- "can",
13035
- "could",
13036
- "did",
13037
- "do",
13038
- "does",
13039
- "doing",
13040
- "down",
13041
- "during",
13042
- "each",
13043
- "either",
13044
- "every",
13045
- "few",
13046
- "for",
13047
- "from",
13048
- "further",
13049
- "get",
13050
- "got",
13051
- "had",
13052
- "has",
13053
- "have",
13054
- "having",
13055
- "he",
13056
- "her",
13057
- "here",
13058
- "hers",
13059
- "herself",
13060
- "him",
13061
- "himself",
13062
- "his",
13063
- "how",
13064
- "i",
13065
- "if",
13066
- "in",
13067
- "into",
13068
- "is",
13069
- "it",
13070
- "its",
13071
- "itself",
13072
- "just",
13073
- "may",
13074
- "me",
13075
- "might",
13076
- "more",
13077
- "most",
13078
- "must",
13079
- "my",
13080
- "myself",
13081
- "neither",
13082
- "no",
13083
- "nor",
13084
- "not",
13085
- "now",
13086
- "of",
13087
- "off",
13088
- "on",
13089
- "once",
13090
- "only",
13091
- "or",
13092
- "other",
13093
- "ought",
13094
- "our",
13095
- "ours",
13096
- "ourselves",
13097
- "out",
13098
- "over",
13099
- "own",
13100
- "same",
13101
- "shall",
13102
- "she",
13103
- "should",
13104
- "so",
13105
- "some",
13106
- "such",
13107
- "than",
13108
- "that",
13109
- "the",
13110
- "their",
13111
- "theirs",
13112
- "them",
13113
- "themselves",
13114
- "then",
13115
- "there",
13116
- "these",
13117
- "they",
13118
- "this",
13119
- "those",
13120
- "through",
13121
- "to",
13122
- "too",
13123
- "under",
13124
- "until",
13125
- "up",
13126
- "us",
13127
- "very",
13128
- "was",
13129
- "we",
13130
- "were",
13131
- "what",
13132
- "when",
13133
- "where",
13134
- "which",
13135
- "while",
13136
- "who",
13137
- "whom",
13138
- "why",
13139
- "will",
13140
- "with",
13141
- "would",
13142
- "yet",
13143
- "you",
13144
- "your",
13145
- "yours",
13146
- "yourself",
13147
- "yourselves"
13148
- ]);
13149
- var STEM_RULES = [
13150
- // Step 1: plurals and past participles
13151
- [/ies$/, "i", 3],
13152
- // policies → polici,eries → eri
13153
- [/sses$/, "ss", 4],
13154
- // addresses → address
13155
- [/([^s])s$/, "$1", 3],
13156
- // items → item, but not "ss"
13157
- [/eed$/, "ee", 4],
13158
- // agreed → agree
13159
- [/ed$/, "", 3],
13160
- // configured → configur, but min length 3
13161
- [/ing$/, "", 4],
13162
- // running → runn → run (handled below)
13163
- // Step 2: derivational suffixes
13164
- [/ational$/, "ate", 6],
13165
- // relational → relate
13166
- [/tion$/, "t", 5],
13167
- // adoption → adopt
13168
- [/ness$/, "", 5],
13169
- // awareness → aware
13170
- [/ment$/, "", 5],
13171
- // deployment → deploy
13172
- [/able$/, "", 5],
13173
- // configurable → configur
13174
- [/ible$/, "", 5],
13175
- // accessible → access
13176
- [/ful$/, "", 5],
13177
- // powerful → power
13178
- [/ous$/, "", 5],
13179
- // dangerous → danger
13180
- [/ive$/, "", 5],
13181
- // interactive → interact
13182
- [/ize$/, "", 4],
13183
- // normalize → normal
13184
- [/ise$/, "", 4],
13185
- // organise → organ
13186
- [/ally$/, "", 5],
13187
- // automatically → automat
13188
- [/ly$/, "", 4],
13189
- // quickly → quick
13190
- [/er$/, "", 4]
13191
- // handler → handl
13192
- ];
13193
- var DOUBLE_CONSONANT = /([^aeiou])\1$/;
13194
- function stem(word) {
13195
- if (word.length < 3) return word;
13196
- let stemmed = word;
13197
- for (const [pattern, replacement, minLen] of STEM_RULES) {
13198
- if (stemmed.length >= minLen && pattern.test(stemmed)) {
13199
- stemmed = stemmed.replace(pattern, replacement);
13200
- break;
13201
- }
13202
- }
13203
- if (stemmed.length > 2 && DOUBLE_CONSONANT.test(stemmed)) {
13204
- stemmed = stemmed.slice(0, -1);
13205
- }
13206
- return stemmed;
13207
- }
13208
- function tokenize(text) {
13209
- return text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOP_WORDS.has(t)).map(stem);
13210
- }
13211
- var MemorySearchIndex = class {
13212
- /** Posting lists: stemmed term → Set of memory IDs containing it */
13213
- postings = /* @__PURE__ */ new Map();
13214
- /** Per-document metadata for BM25 scoring */
13215
- docs = /* @__PURE__ */ new Map();
13216
- /** Pre-computed IDF values. Stale flag triggers lazy recomputation. */
13217
- idf = /* @__PURE__ */ new Map();
13218
- idfStale = true;
13219
- /** 3-character prefix map for prefix matching: prefix → Set of full stems */
13220
- prefixMap = /* @__PURE__ */ new Map();
13221
- /** Total weighted document length (for computing average) */
13222
- totalWeightedLen = 0;
13223
- get docCount() {
13224
- return this.docs.size;
13225
- }
13226
- get avgDocLen() {
13227
- return this.docs.size > 0 ? this.totalWeightedLen / this.docs.size : 1;
13228
- }
13229
- /**
13230
- * Index a memory entry. Extracts stems from title, content, and tags
13231
- * with field-specific weighting and builds posting lists.
13232
- */
13233
- addDocument(id, entry) {
13234
- if (this.docs.has(id)) this.removeDocument(id);
13235
- const titleTokens = tokenize(entry.title);
13236
- const contentTokens = tokenize(entry.content);
13237
- const tagTokens = entry.tags.flatMap((t) => tokenize(t));
13238
- const weightedTf = /* @__PURE__ */ new Map();
13239
- for (const t of titleTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TITLE);
13240
- for (const t of tagTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_TAGS);
13241
- for (const t of contentTokens) weightedTf.set(t, (weightedTf.get(t) || 0) + FIELD_WEIGHT_CONTENT);
13242
- const weightedLen = titleTokens.length * FIELD_WEIGHT_TITLE + tagTokens.length * FIELD_WEIGHT_TAGS + contentTokens.length * FIELD_WEIGHT_CONTENT;
13243
- const allStems = /* @__PURE__ */ new Set();
13244
- for (const t of weightedTf.keys()) allStems.add(t);
13245
- const stemSequence = [...titleTokens, ...contentTokens];
13246
- const docRecord = { weightedTf, weightedLen, allStems, stemSequence };
13247
- this.docs.set(id, docRecord);
13248
- this.totalWeightedLen += weightedLen;
13249
- for (const term of allStems) {
13250
- let posting = this.postings.get(term);
13251
- if (!posting) {
13252
- posting = /* @__PURE__ */ new Set();
13253
- this.postings.set(term, posting);
13254
- }
13255
- posting.add(id);
13256
- if (term.length >= 3) {
13257
- const prefix = term.slice(0, 3);
13258
- let prefixSet = this.prefixMap.get(prefix);
13259
- if (!prefixSet) {
13260
- prefixSet = /* @__PURE__ */ new Set();
13261
- this.prefixMap.set(prefix, prefixSet);
13262
- }
13263
- prefixSet.add(term);
13264
- }
13265
- }
13266
- this.idfStale = true;
13267
- }
13268
- /** Remove a document from the index. */
13269
- removeDocument(id) {
13270
- const doc = this.docs.get(id);
13271
- if (!doc) return;
13272
- this.totalWeightedLen -= doc.weightedLen;
13273
- this.docs.delete(id);
13274
- for (const term of doc.allStems) {
13275
- const posting = this.postings.get(term);
13276
- if (posting) {
13277
- posting.delete(id);
13278
- if (posting.size === 0) {
13279
- this.postings.delete(term);
13280
- if (term.length >= 3) {
13281
- const prefixSet = this.prefixMap.get(term.slice(0, 3));
13282
- if (prefixSet) {
13283
- prefixSet.delete(term);
13284
- if (prefixSet.size === 0) this.prefixMap.delete(term.slice(0, 3));
13285
- }
13286
- }
13287
- }
13288
- }
13289
- }
13290
- this.idfStale = true;
13291
- }
13292
- /** Recompute IDF values for all terms. Called lazily before search. */
13293
- refreshIdf() {
13294
- if (!this.idfStale) return;
13295
- const N = this.docs.size;
13296
- this.idf.clear();
13297
- for (const [term, posting] of this.postings) {
13298
- const df = posting.size;
13299
- this.idf.set(term, Math.log((N - df + 0.5) / (df + 0.5) + 1));
13300
- }
13301
- this.idfStale = false;
13302
- }
13303
- /**
13304
- * Expand query terms with prefix matches.
13305
- * "deploy" → ["deploy", "deployment", "deploying", ...] (if they exist in the index)
13306
- */
13307
- expandQueryTerms(queryStems) {
13308
- const expanded = /* @__PURE__ */ new Map();
13309
- for (const qs of queryStems) {
13310
- if (this.postings.has(qs)) {
13311
- expanded.set(qs, Math.max(expanded.get(qs) || 0, 1));
13312
- }
13313
- if (qs.length >= 3) {
13314
- const prefix = qs.slice(0, 3);
13315
- const candidates = this.prefixMap.get(prefix);
13316
- if (candidates) {
13317
- for (const candidate of candidates) {
13318
- if (candidate !== qs && candidate.startsWith(qs)) {
13319
- expanded.set(candidate, Math.max(expanded.get(candidate) || 0, PREFIX_MATCH_PENALTY));
13320
- }
13321
- }
13322
- }
13323
- }
13324
- }
13325
- return expanded;
13326
- }
13327
- /**
13328
- * Compute bigram proximity boost: if two query terms appear adjacent
13329
- * in the document's stem sequence, boost the score.
13330
- */
13331
- bigramProximityBoost(docId, queryStems) {
13332
- if (queryStems.length < 2) return 0;
13333
- const doc = this.docs.get(docId);
13334
- if (!doc || doc.stemSequence.length < 2) return 0;
13335
- let boost = 0;
13336
- const seq = doc.stemSequence;
13337
- const querySet = new Set(queryStems);
13338
- for (let i = 0; i < seq.length - 1; i++) {
13339
- if (querySet.has(seq[i]) && querySet.has(seq[i + 1]) && seq[i] !== seq[i + 1]) {
13340
- boost += 0.5;
13341
- }
13342
- }
13343
- return Math.min(boost, 2);
13344
- }
13345
- /**
13346
- * Search the index for documents matching a query.
13347
- * Returns scored results sorted by BM25F relevance.
13348
- *
13349
- * @param query - Raw query string
13350
- * @param candidateIds - Optional: only score these document IDs (for agent-scoped search)
13351
- * @returns Array of { id, score } sorted by descending score
13352
- */
13353
- search(query, candidateIds) {
13354
- const queryStems = tokenize(query);
13355
- if (queryStems.length === 0) return [];
13356
- this.refreshIdf();
13357
- const expandedTerms = this.expandQueryTerms(queryStems);
13358
- if (expandedTerms.size === 0) return [];
13359
- const avgDl = this.avgDocLen;
13360
- const candidates = /* @__PURE__ */ new Set();
13361
- for (const term of expandedTerms.keys()) {
13362
- const posting = this.postings.get(term);
13363
- if (posting) {
13364
- for (const docId of posting) {
13365
- if (!candidateIds || candidateIds.has(docId)) candidates.add(docId);
13366
- }
13367
- }
13368
- }
13369
- const results = [];
13370
- for (const docId of candidates) {
13371
- const doc = this.docs.get(docId);
13372
- if (!doc) continue;
13373
- let score = 0;
13374
- for (const [term, weight] of expandedTerms) {
13375
- const tf = doc.weightedTf.get(term) || 0;
13376
- if (tf === 0) continue;
13377
- const termIdf = this.idf.get(term) || 0;
13378
- const numerator = tf * (BM25_K1 + 1);
13379
- const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.weightedLen / avgDl));
13380
- score += termIdf * (numerator / denominator) * weight;
13381
- }
13382
- score += this.bigramProximityBoost(docId, queryStems);
13383
- if (score > 0) results.push({ id: docId, score });
13384
- }
13385
- results.sort((a, b) => b.score - a.score);
13386
- return results;
13387
- }
13388
- /** Check if a document exists in the index. */
13389
- has(id) {
13390
- return this.docs.has(id);
13391
- }
13392
- };
13393
-
13394
- // src/memory/manager.ts
13395
13151
  function sj(v, fb = {}) {
13396
13152
  if (!v) return fb;
13397
13153
  try {
@@ -13903,6 +13659,7 @@ export {
13903
13659
  GET_DATETIME_TOOL,
13904
13660
  GatewayManager,
13905
13661
  InboxWatcher,
13662
+ LOAD_SKILL_TOOL,
13906
13663
  MEMORY_CATEGORIES,
13907
13664
  MailReceiver,
13908
13665
  MailSender,
@@ -13939,6 +13696,7 @@ export {
13939
13696
  RelayBridge,
13940
13697
  RelayGateway,
13941
13698
  SEARCH_EMAIL_TOOL,
13699
+ SEARCH_SKILLS_TOOL,
13942
13700
  SPAM_THRESHOLD,
13943
13701
  ServiceManager,
13944
13702
  SetupManager,
@@ -14018,6 +13776,7 @@ export {
14018
13776
  getTelegramWebhookInfo,
14019
13777
  hostSessionStoragePath,
14020
13778
  inferPhoneRegion,
13779
+ invalidateSkillCache,
14021
13780
  isInternalEmail,
14022
13781
  isLoopbackMailHost,
14023
13782
  isOperatorReplySender,
@@ -14026,7 +13785,9 @@ export {
14026
13785
  isTelegramChatAllowed,
14027
13786
  isTelegramStopCommand,
14028
13787
  isValidPhoneNumber,
13788
+ listSkills,
14029
13789
  loadHostSession,
13790
+ loadSkill,
14030
13791
  mapProviderSmsStatus,
14031
13792
  nextTelegramOffset,
14032
13793
  normalizeAddress,
@@ -14051,6 +13812,7 @@ export {
14051
13812
  redactSecret,
14052
13813
  redactSmsConfig,
14053
13814
  redactTelegramConfig,
13815
+ renderSkillAsPrompt,
14054
13816
  requireBinary,
14055
13817
  requireWhisperModel,
14056
13818
  resolveConfig,
@@ -14059,8 +13821,10 @@ export {
14059
13821
  sanitizeEmail,
14060
13822
  saveConfig,
14061
13823
  saveHostSession,
13824
+ saveUserSkill,
14062
13825
  scanOutboundEmail,
14063
13826
  scoreEmail,
13827
+ searchSkills,
14064
13828
  sendTelegramMessage,
14065
13829
  setOperatorEmail,
14066
13830
  setTelegramWebhook,
@@ -14073,10 +13837,12 @@ export {
14073
13837
  threadIdFor,
14074
13838
  tokenize,
14075
13839
  tryJoin,
13840
+ userSkillsDir,
14076
13841
  validateApiUrl,
14077
13842
  validatePhoneMissionPolicy,
14078
13843
  validatePhoneMissionStart,
14079
13844
  validatePhoneTransportProfile,
13845
+ validateSkill,
14080
13846
  validateTwilioSignature,
14081
13847
  webSearch
14082
13848
  };