@agenticmail/core 0.9.24 → 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";
@@ -1109,14 +1122,14 @@ var StalwartAdmin = class {
1109
1122
  if (!isValidDomain(domain)) {
1110
1123
  throw new Error(`Invalid domain format: "${domain}"`);
1111
1124
  }
1112
- const { readFileSync: readFileSync11, writeFileSync: writeFileSync12 } = await import("fs");
1113
- const { homedir: homedir14 } = await import("os");
1114
- const { join: join17 } = await import("path");
1115
- const configPath = join17(homedir14(), ".agenticmail", "stalwart.toml");
1125
+ const { readFileSync: readFileSync10, writeFileSync: writeFileSync11 } = await import("fs");
1126
+ const { homedir: homedir13 } = await import("os");
1127
+ const { join: join16 } = await import("path");
1128
+ const configPath = join16(homedir13(), ".agenticmail", "stalwart.toml");
1116
1129
  try {
1117
- let config = readFileSync11(configPath, "utf-8");
1130
+ let config = readFileSync10(configPath, "utf-8");
1118
1131
  config = config.replace(/^hostname\s*=\s*"[^"]*"/m, `hostname = "${escapeTomlString(domain)}"`);
1119
- writeFileSync12(configPath, config);
1132
+ writeFileSync11(configPath, config);
1120
1133
  console.log(`[Stalwart] Updated hostname to "${domain}" in stalwart.toml`);
1121
1134
  } catch (err) {
1122
1135
  throw new Error(`Failed to set config server.hostname=${domain}`);
@@ -1125,15 +1138,15 @@ var StalwartAdmin = class {
1125
1138
  // --- DKIM ---
1126
1139
  /** Path to the host-side stalwart.toml (mounted read-only into container) */
1127
1140
  get configPath() {
1128
- const { homedir: homedir14 } = __require("os");
1129
- const { join: join17 } = __require("path");
1130
- return join17(homedir14(), ".agenticmail", "stalwart.toml");
1141
+ const { homedir: homedir13 } = __require("os");
1142
+ const { join: join16 } = __require("path");
1143
+ return join16(homedir13(), ".agenticmail", "stalwart.toml");
1131
1144
  }
1132
1145
  /** Path to host-side DKIM key directory */
1133
1146
  get dkimDir() {
1134
- const { homedir: homedir14 } = __require("os");
1135
- const { join: join17 } = __require("path");
1136
- return join17(homedir14(), ".agenticmail");
1147
+ const { homedir: homedir13 } = __require("os");
1148
+ const { join: join16 } = __require("path");
1149
+ return join16(homedir13(), ".agenticmail");
1137
1150
  }
1138
1151
  /**
1139
1152
  * Create/reuse a DKIM signing key for a domain.
@@ -1234,12 +1247,12 @@ var StalwartAdmin = class {
1234
1247
  * This bypasses the need for a PTR record on the sending IP.
1235
1248
  */
1236
1249
  async configureOutboundRelay(config) {
1237
- const { readFileSync: readFileSync11, writeFileSync: writeFileSync12 } = await import("fs");
1238
- const { homedir: homedir14 } = await import("os");
1239
- const { join: join17 } = await import("path");
1250
+ const { readFileSync: readFileSync10, writeFileSync: writeFileSync11 } = await import("fs");
1251
+ const { homedir: homedir13 } = await import("os");
1252
+ const { join: join16 } = await import("path");
1240
1253
  const routeName = config.routeName ?? "gmail";
1241
- const tomlPath = join17(homedir14(), ".agenticmail", "stalwart.toml");
1242
- let toml = readFileSync11(tomlPath, "utf-8");
1254
+ const tomlPath = join16(homedir13(), ".agenticmail", "stalwart.toml");
1255
+ let toml = readFileSync10(tomlPath, "utf-8");
1243
1256
  toml = toml.replace(/\n\[queue\.route\.gmail\][\s\S]*?(?=\n\[|$)/, "");
1244
1257
  toml = toml.replace(/\n\[queue\.strategy\][\s\S]*?(?=\n\[|$)/, "");
1245
1258
  const safeRouteName = routeName.replace(/[^a-zA-Z0-9_-]/g, "");
@@ -1259,7 +1272,7 @@ auth.secret = "${escapeTomlString(config.password)}"
1259
1272
  route = [ { if = "is_local_domain('', rcpt_domain)", then = "'local'" },
1260
1273
  { else = "'${safeRouteName}'" } ]
1261
1274
  `;
1262
- writeFileSync12(tomlPath, toml, "utf-8");
1275
+ writeFileSync11(tomlPath, toml, "utf-8");
1263
1276
  await this.restartContainer();
1264
1277
  }
1265
1278
  };
@@ -7460,12 +7473,12 @@ var GatewayManager = class {
7460
7473
  zone = await this.cfClient.createZone(domain);
7461
7474
  }
7462
7475
  const existingRecords = await this.cfClient.listDnsRecords(zone.id);
7463
- const { homedir: homedir14 } = await import("os");
7464
- const backupDir = join4(homedir14(), ".agenticmail");
7476
+ const { homedir: homedir13 } = await import("os");
7477
+ const backupDir = join4(homedir13(), ".agenticmail");
7465
7478
  const backupPath = join4(backupDir, `dns-backup-${domain}-${Date.now()}.json`);
7466
- const { writeFileSync: writeFileSync12, mkdirSync: mkdirSync13 } = await import("fs");
7467
- mkdirSync13(backupDir, { recursive: true });
7468
- writeFileSync12(backupPath, JSON.stringify({
7479
+ const { writeFileSync: writeFileSync11, mkdirSync: mkdirSync12 } = await import("fs");
7480
+ mkdirSync12(backupDir, { recursive: true });
7481
+ writeFileSync11(backupPath, JSON.stringify({
7469
7482
  domain,
7470
7483
  zoneId: zone.id,
7471
7484
  backedUpAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -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 {
@@ -13874,284 +13630,6 @@ var AgentMemoryManager = class {
13874
13630
  };
13875
13631
  }
13876
13632
  };
13877
-
13878
- // src/skills/registry.ts
13879
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync11 } from "fs";
13880
- import { join as join16, dirname as dirname3, basename as basename2 } from "path";
13881
- import { fileURLToPath } from "url";
13882
- import { homedir as homedir13 } from "os";
13883
- function builtInDir() {
13884
- const here = dirname3(fileURLToPath(import.meta.url));
13885
- return join16(here, "built-in");
13886
- }
13887
- function userDir() {
13888
- const dir2 = join16(homedir13(), ".agenticmail", "skills");
13889
- try {
13890
- if (!existsSync14(dir2)) mkdirSync12(dir2, { recursive: true });
13891
- } catch {
13892
- }
13893
- return dir2;
13894
- }
13895
- var cache = { ts: 0, byId: null };
13896
- var CACHE_TTL_MS = 5e3;
13897
- function loadAllSkillsFromDisk() {
13898
- const all = /* @__PURE__ */ new Map();
13899
- const dirs = [builtInDir(), userDir()];
13900
- for (const dir2 of dirs) {
13901
- if (!existsSync14(dir2)) continue;
13902
- let entries;
13903
- try {
13904
- entries = readdirSync3(dir2);
13905
- } catch {
13906
- continue;
13907
- }
13908
- for (const entry of entries) {
13909
- if (!entry.endsWith(".json")) continue;
13910
- const fullPath = join16(dir2, entry);
13911
- try {
13912
- const st = statSync3(fullPath);
13913
- if (!st.isFile()) continue;
13914
- const raw = readFileSync10(fullPath, "utf-8");
13915
- const parsed = JSON.parse(raw);
13916
- const errors = validateSkill(parsed);
13917
- if (errors.length > 0) {
13918
- console.warn(`[skills] ${entry} invalid, skipping: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
13919
- continue;
13920
- }
13921
- all.set(parsed.id, parsed);
13922
- } catch (err) {
13923
- console.warn(`[skills] could not load ${fullPath}: ${err.message}`);
13924
- }
13925
- }
13926
- }
13927
- return all;
13928
- }
13929
- function ensureLoaded() {
13930
- const now = Date.now();
13931
- if (cache.byId && now - cache.ts < CACHE_TTL_MS) return cache.byId;
13932
- const fresh = loadAllSkillsFromDisk();
13933
- cache.byId = fresh;
13934
- cache.ts = now;
13935
- return fresh;
13936
- }
13937
- function invalidateSkillCache() {
13938
- cache.byId = null;
13939
- cache.ts = 0;
13940
- }
13941
- function validateSkill(s) {
13942
- const errs = [];
13943
- if (!s || typeof s !== "object" || Array.isArray(s)) {
13944
- return [{ path: "$", message: "skill must be a JSON object" }];
13945
- }
13946
- const sk = s;
13947
- const requireString = (key, minLen = 1) => {
13948
- const v = sk[key];
13949
- if (typeof v !== "string" || v.length < minLen) {
13950
- errs.push({ path: key, message: `must be a non-empty string` });
13951
- }
13952
- };
13953
- const requireArray = (key, minLen = 1) => {
13954
- const v = sk[key];
13955
- if (!Array.isArray(v) || v.length < minLen) {
13956
- errs.push({ path: key, message: `must be a non-empty array` });
13957
- }
13958
- };
13959
- requireString("id");
13960
- if (typeof sk.id === "string" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(sk.id)) {
13961
- errs.push({ path: "id", message: "must be lowercase-hyphenated slug (a-z, 0-9, -)" });
13962
- }
13963
- requireString("name");
13964
- requireString("version");
13965
- requireString("description");
13966
- requireString("category");
13967
- const validCategories = [
13968
- "negotiation",
13969
- "customer-service",
13970
- "reservations",
13971
- "medical-admin",
13972
- "legal-admin",
13973
- "finance-admin",
13974
- "real-estate",
13975
- "travel",
13976
- "subscription",
13977
- "home-services",
13978
- "social",
13979
- "civic",
13980
- "employment",
13981
- "debt-collection",
13982
- "other"
13983
- ];
13984
- if (typeof sk.category === "string" && !validCategories.includes(sk.category)) {
13985
- errs.push({ path: "category", message: `unknown category "${sk.category}"; must be one of: ${validCategories.join(", ")}` });
13986
- }
13987
- if (!Array.isArray(sk.tags)) errs.push({ path: "tags", message: "must be an array of strings" });
13988
- if (sk.disclaimer !== null && typeof sk.disclaimer !== "string") {
13989
- errs.push({ path: "disclaimer", message: "must be a string or null" });
13990
- }
13991
- if (!sk.context || typeof sk.context !== "object") {
13992
- errs.push({ path: "context", message: "must be an object" });
13993
- } else {
13994
- const ctx = sk.context;
13995
- if (typeof ctx.when_to_use !== "string") errs.push({ path: "context.when_to_use", message: "must be a string" });
13996
- if (!Array.isArray(ctx.preconditions)) errs.push({ path: "context.preconditions", message: "must be an array" });
13997
- if (typeof ctx.estimated_call_duration_minutes !== "number") errs.push({ path: "context.estimated_call_duration_minutes", message: "must be a number" });
13998
- }
13999
- requireArray("principles", 2);
14000
- if (!sk.phrases || typeof sk.phrases !== "object") errs.push({ path: "phrases", message: "must be an object of {key: phrase}" });
14001
- if (!Array.isArray(sk.tactics) || sk.tactics.length === 0) {
14002
- errs.push({ path: "tactics", message: "must be a non-empty array" });
14003
- } else {
14004
- sk.tactics.forEach((t, i) => {
14005
- if (!t || typeof t !== "object") {
14006
- errs.push({ path: `tactics[${i}]`, message: "must be an object" });
14007
- return;
14008
- }
14009
- const tactic = t;
14010
- if (typeof tactic.name !== "string") errs.push({ path: `tactics[${i}].name`, message: "must be a string" });
14011
- if (typeof tactic.when !== "string") errs.push({ path: `tactics[${i}].when`, message: "must be a string" });
14012
- if (typeof tactic.script !== "string" || tactic.script.length < 5) {
14013
- errs.push({ path: `tactics[${i}].script`, message: "must be a non-empty string (>= 5 chars)" });
14014
- }
14015
- });
14016
- }
14017
- requireArray("boundaries", 1);
14018
- if (!Array.isArray(sk.success_signals)) errs.push({ path: "success_signals", message: "must be an array" });
14019
- if (!Array.isArray(sk.failure_signals)) errs.push({ path: "failure_signals", message: "must be an array" });
14020
- if (!sk.exit_strategy || typeof sk.exit_strategy !== "object") {
14021
- errs.push({ path: "exit_strategy", message: "must be an object" });
14022
- } else {
14023
- const xs = sk.exit_strategy;
14024
- if (typeof xs.on_success !== "string") errs.push({ path: "exit_strategy.on_success", message: "must be a string" });
14025
- if (typeof xs.on_failure !== "string") errs.push({ path: "exit_strategy.on_failure", message: "must be a string" });
14026
- }
14027
- if (!Array.isArray(sk.required_user_info)) errs.push({ path: "required_user_info", message: "must be an array" });
14028
- if (typeof sk.contributed_by !== "string") errs.push({ path: "contributed_by", message: "must be a string" });
14029
- return errs;
14030
- }
14031
- function summarize(s) {
14032
- return {
14033
- id: s.id,
14034
- name: s.name,
14035
- category: s.category,
14036
- tags: s.tags,
14037
- description: s.description,
14038
- version: s.version,
14039
- disclaimer_required: !!s.disclaimer,
14040
- estimated_call_duration_minutes: s.context.estimated_call_duration_minutes
14041
- };
14042
- }
14043
- function listSkills(opts = {}) {
14044
- const all = Array.from(ensureLoaded().values());
14045
- const filtered = all.filter((s) => {
14046
- if (opts.category && s.category !== opts.category) return false;
14047
- if (opts.tag && !s.tags.includes(opts.tag.toLowerCase())) return false;
14048
- return true;
14049
- });
14050
- return filtered.sort((a, b) => a.name.localeCompare(b.name)).map(summarize);
14051
- }
14052
- function searchSkills(query, limit = 20) {
14053
- const q = query.toLowerCase().trim();
14054
- if (!q) return listSkills();
14055
- const all = Array.from(ensureLoaded().values());
14056
- const scored = [];
14057
- for (const s of all) {
14058
- let score = 0;
14059
- if (s.id.toLowerCase().includes(q)) score += 10;
14060
- if (s.name.toLowerCase().includes(q)) score += 8;
14061
- if (s.tags.some((t) => t.toLowerCase().includes(q))) score += 5;
14062
- if (s.category.toLowerCase().includes(q)) score += 4;
14063
- if (s.description.toLowerCase().includes(q)) score += 3;
14064
- if (s.principles.some((p) => p.toLowerCase().includes(q))) score += 2;
14065
- if (Object.values(s.phrases).some((p) => p.toLowerCase().includes(q))) score += 2;
14066
- if (s.tactics.some((t) => t.script.toLowerCase().includes(q) || t.name.toLowerCase().includes(q))) score += 2;
14067
- if (score > 0) scored.push({ skill: s, score });
14068
- }
14069
- scored.sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name));
14070
- return scored.slice(0, limit).map((x) => summarize(x.skill));
14071
- }
14072
- function loadSkill(id) {
14073
- return ensureLoaded().get(id) ?? null;
14074
- }
14075
- function saveUserSkill(skill) {
14076
- const errors = validateSkill(skill);
14077
- if (errors.length > 0) {
14078
- throw new Error(`skill validation failed: ${errors.map((e) => `${e.path}: ${e.message}`).join("; ")}`);
14079
- }
14080
- const dir2 = userDir();
14081
- const path2 = join16(dir2, `${skill.id}.json`);
14082
- const now = (/* @__PURE__ */ new Date()).toISOString();
14083
- const out = {
14084
- ...skill,
14085
- created_at: skill.created_at ?? now,
14086
- updated_at: now
14087
- };
14088
- writeFileSync11(path2, JSON.stringify(out, null, 2), "utf-8");
14089
- invalidateSkillCache();
14090
- return { path: path2 };
14091
- }
14092
- function userSkillsDir() {
14093
- return userDir();
14094
- }
14095
-
14096
- // src/skills/index.ts
14097
- function renderSkillAsPrompt(skill) {
14098
- const lines = [];
14099
- lines.push(`=== SKILL LOADED: ${skill.name} (v${skill.version}) ===`);
14100
- lines.push(`Category: ${skill.category} Tags: ${skill.tags.join(", ")}`);
14101
- lines.push("");
14102
- lines.push(skill.description);
14103
- lines.push("");
14104
- if (skill.disclaimer) {
14105
- lines.push("REQUIRED DISCLAIMER (recite at start of the substantive turn):");
14106
- lines.push(` "${skill.disclaimer}"`);
14107
- lines.push("");
14108
- }
14109
- lines.push("WHEN TO USE THIS:");
14110
- lines.push(` ${skill.context.when_to_use}`);
14111
- if (skill.context.preconditions.length > 0) {
14112
- lines.push("Preconditions:");
14113
- for (const p of skill.context.preconditions) lines.push(` - ${p}`);
14114
- }
14115
- lines.push("");
14116
- lines.push("PRINCIPLES:");
14117
- for (const p of skill.principles) lines.push(` - ${p}`);
14118
- lines.push("");
14119
- if (Object.keys(skill.phrases).length > 0) {
14120
- lines.push("PHRASES (paraphrase to match your voice; keep the structural move):");
14121
- for (const [k, v] of Object.entries(skill.phrases)) lines.push(` [${k}] "${v}"`);
14122
- lines.push("");
14123
- }
14124
- if (skill.tactics.length > 0) {
14125
- lines.push("TACTICS (try in order; fall back to next on failure):");
14126
- skill.tactics.forEach((t, i) => {
14127
- lines.push(` ${i + 1}. ${t.name}`);
14128
- lines.push(` When: ${t.when}`);
14129
- lines.push(` Script: "${t.script}"`);
14130
- });
14131
- lines.push("");
14132
- }
14133
- if (skill.boundaries.length > 0) {
14134
- lines.push("HARD BOUNDARIES \u2014 do not cross:");
14135
- for (const b of skill.boundaries) lines.push(` - ${b}`);
14136
- lines.push("");
14137
- }
14138
- lines.push("SUCCESS SIGNALS:");
14139
- for (const s of skill.success_signals) lines.push(` - ${s}`);
14140
- lines.push("");
14141
- lines.push("FAILURE SIGNALS \u2014 when these appear, escalate or exit:");
14142
- for (const s of skill.failure_signals) lines.push(` - ${s}`);
14143
- lines.push("");
14144
- lines.push("EXIT:");
14145
- lines.push(` On success: ${skill.exit_strategy.on_success}`);
14146
- lines.push(` On failure: ${skill.exit_strategy.on_failure}`);
14147
- if (skill.exit_strategy.follow_ups && skill.exit_strategy.follow_ups.length > 0) {
14148
- lines.push(" Follow-ups (after the call):");
14149
- for (const f of skill.exit_strategy.follow_ups) lines.push(` - ${f}`);
14150
- }
14151
- lines.push("");
14152
- lines.push("=== END SKILL ===");
14153
- return lines.join("\n");
14154
- }
14155
13633
  export {
14156
13634
  AGENT_ROLES,
14157
13635
  ASK_OPERATOR_TOOL,
@@ -14181,6 +13659,7 @@ export {
14181
13659
  GET_DATETIME_TOOL,
14182
13660
  GatewayManager,
14183
13661
  InboxWatcher,
13662
+ LOAD_SKILL_TOOL,
14184
13663
  MEMORY_CATEGORIES,
14185
13664
  MailReceiver,
14186
13665
  MailSender,
@@ -14217,6 +13696,7 @@ export {
14217
13696
  RelayBridge,
14218
13697
  RelayGateway,
14219
13698
  SEARCH_EMAIL_TOOL,
13699
+ SEARCH_SKILLS_TOOL,
14220
13700
  SPAM_THRESHOLD,
14221
13701
  ServiceManager,
14222
13702
  SetupManager,