@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/chunk-J6JINNJ3.js +724 -0
- package/dist/index.cjs +1173 -663
- package/dist/index.d.cts +366 -1
- package/dist/index.d.ts +366 -1
- package/dist/index.js +161 -395
- package/dist/skills/built-in/airline-change-or-refund.json +111 -0
- package/dist/skills/built-in/book-medical-appointment.json +99 -0
- package/dist/skills/built-in/book-restaurant-reservation.json +93 -0
- package/dist/skills/built-in/cancel-subscription-graceful.json +101 -0
- package/dist/skills/built-in/court-administrative-checkin.json +99 -0
- package/dist/skills/built-in/dispute-credit-card-charge.json +105 -0
- package/dist/skills/built-in/handle-debt-collector.json +109 -0
- package/dist/skills/built-in/negotiate-bill-reduction.json +116 -0
- package/dist/skills/built-in/schedule-home-service.json +100 -0
- package/dist/skills-RE3S767B.js +23 -0
- package/package.json +1 -1
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
|
};
|