@agenticmail/core 0.9.37 → 0.9.38

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.
@@ -575,8 +575,8 @@ function validateSkill(s) {
575
575
  if (typeof sk.contributed_by !== "string") errs.push({ path: "contributed_by", message: "must be a string" });
576
576
  return errs;
577
577
  }
578
- function summarize(s) {
579
- return {
578
+ function summarize(s, score) {
579
+ const out = {
580
580
  id: s.id,
581
581
  name: s.name,
582
582
  category: s.category,
@@ -584,8 +584,12 @@ function summarize(s) {
584
584
  description: s.description,
585
585
  version: s.version,
586
586
  disclaimer_required: !!s.disclaimer,
587
- estimated_call_duration_minutes: s.context.estimated_call_duration_minutes
587
+ estimated_call_duration_minutes: s.context.estimated_call_duration_minutes,
588
+ when_to_use: s.context.when_to_use,
589
+ first_principle: s.principles && s.principles.length > 0 ? s.principles[0] : ""
588
590
  };
591
+ if (score !== void 0) out.score = score;
592
+ return out;
589
593
  }
590
594
  function listSkills(opts = {}) {
591
595
  const all = Array.from(ensureLoaded().byId.values());
@@ -606,17 +610,17 @@ function searchSkills(query, limit = 20) {
606
610
  const fallback = [];
607
611
  for (const s of byId.values()) {
608
612
  if (s.id.toLowerCase().includes(qLow) || s.name.toLowerCase().includes(qLow) || s.tags.some((t) => t.toLowerCase().includes(qLow))) {
609
- fallback.push(summarize(s));
613
+ fallback.push(summarize(s, 0.1));
610
614
  if (fallback.length >= limit) break;
611
615
  }
612
616
  }
613
617
  return fallback;
614
618
  }
615
619
  const out = [];
616
- for (const { id } of ranked) {
620
+ for (const { id, score } of ranked) {
617
621
  const skill = byId.get(id);
618
622
  if (!skill) continue;
619
- out.push(summarize(skill));
623
+ out.push(summarize(skill, score));
620
624
  if (out.length >= limit) break;
621
625
  }
622
626
  return out;
package/dist/index.cjs CHANGED
@@ -1280,8 +1280,8 @@ function validateSkill(s) {
1280
1280
  if (typeof sk.contributed_by !== "string") errs.push({ path: "contributed_by", message: "must be a string" });
1281
1281
  return errs;
1282
1282
  }
1283
- function summarize(s) {
1284
- return {
1283
+ function summarize(s, score) {
1284
+ const out = {
1285
1285
  id: s.id,
1286
1286
  name: s.name,
1287
1287
  category: s.category,
@@ -1289,8 +1289,12 @@ function summarize(s) {
1289
1289
  description: s.description,
1290
1290
  version: s.version,
1291
1291
  disclaimer_required: !!s.disclaimer,
1292
- estimated_call_duration_minutes: s.context.estimated_call_duration_minutes
1292
+ estimated_call_duration_minutes: s.context.estimated_call_duration_minutes,
1293
+ when_to_use: s.context.when_to_use,
1294
+ first_principle: s.principles && s.principles.length > 0 ? s.principles[0] : ""
1293
1295
  };
1296
+ if (score !== void 0) out.score = score;
1297
+ return out;
1294
1298
  }
1295
1299
  function listSkills(opts = {}) {
1296
1300
  const all = Array.from(ensureLoaded().byId.values());
@@ -1311,17 +1315,17 @@ function searchSkills(query, limit = 20) {
1311
1315
  const fallback = [];
1312
1316
  for (const s of byId.values()) {
1313
1317
  if (s.id.toLowerCase().includes(qLow) || s.name.toLowerCase().includes(qLow) || s.tags.some((t) => t.toLowerCase().includes(qLow))) {
1314
- fallback.push(summarize(s));
1318
+ fallback.push(summarize(s, 0.1));
1315
1319
  if (fallback.length >= limit) break;
1316
1320
  }
1317
1321
  }
1318
1322
  return fallback;
1319
1323
  }
1320
1324
  const out = [];
1321
- for (const { id } of ranked) {
1325
+ for (const { id, score } of ranked) {
1322
1326
  const skill = byId.get(id);
1323
1327
  if (!skill) continue;
1324
- out.push(summarize(skill));
1328
+ out.push(summarize(skill, score));
1325
1329
  if (out.length >= limit) break;
1326
1330
  }
1327
1331
  return out;
@@ -8678,6 +8682,73 @@ var PhoneManager = class {
8678
8682
  }]);
8679
8683
  return { mission: updated, query: answered, alreadyAnswered: false };
8680
8684
  }
8685
+ /**
8686
+ * v0.9.92 — auto-close stale operator queries.
8687
+ *
8688
+ * An operator query is "stale" when:
8689
+ * - it's unanswered, AND
8690
+ * - either (a) the mission is no longer live (status in
8691
+ * {completed, failed, cancelled}), OR (b) the query is older
8692
+ * than `maxAgeSeconds`.
8693
+ *
8694
+ * The sweeper marks each stale query as answered with a synthetic
8695
+ * "[auto-closed: ...]" answer + `answeredVia: 'auto-sweeper'`. This
8696
+ * (a) gets the query out of `listOpenOperatorQueries`, (b) clears
8697
+ * the bridge's "you have N open questions" hint to the operator,
8698
+ * and (c) leaves a permanent audit trail of WHY the query was
8699
+ * closed.
8700
+ *
8701
+ * Returns the count of queries closed + a per-mission breakdown
8702
+ * for logging.
8703
+ */
8704
+ sweepStaleOperatorQueries(opts = {}) {
8705
+ const maxAgeMs = (opts.maxAgeSeconds ?? 3600) * 1e3;
8706
+ const nowMs = opts.nowMs ?? Date.now();
8707
+ const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
8708
+ const rows = this.db.prepare(
8709
+ `SELECT * FROM phone_missions WHERE metadata_json LIKE '%"operatorQueries"%'`
8710
+ ).all();
8711
+ const breakdown = [];
8712
+ let totalClosed = 0;
8713
+ let missionsTouched = 0;
8714
+ for (const row of rows) {
8715
+ const mission = rowToMission(row);
8716
+ const queries = readOperatorQueries(mission);
8717
+ const open = queries.filter((q) => !q.answer);
8718
+ if (open.length === 0) continue;
8719
+ const reason = TERMINAL.has(mission.status) ? "mission-terminal" : null;
8720
+ const closedAt = new Date(nowMs).toISOString();
8721
+ const nextQueries = queries.map((q) => {
8722
+ if (q.answer) return q;
8723
+ const askedAtMs = Date.parse(q.askedAt);
8724
+ const tooOld = Number.isFinite(askedAtMs) && nowMs - askedAtMs >= maxAgeMs;
8725
+ const queryReason = reason ?? (tooOld ? "age" : null);
8726
+ if (!queryReason) return q;
8727
+ return {
8728
+ ...q,
8729
+ answer: queryReason === "mission-terminal" ? `[auto-closed: mission ended (${mission.status}) before this question was answered]` : `[auto-closed: question went unanswered for over ${Math.round(maxAgeMs / 6e4)} minutes]`,
8730
+ answeredAt: closedAt,
8731
+ answeredVia: "auto-sweeper"
8732
+ };
8733
+ });
8734
+ const closedHere = nextQueries.filter(
8735
+ (q, i) => q.answer && !queries[i].answer && q.answeredVia === "auto-sweeper"
8736
+ ).length;
8737
+ if (closedHere === 0) continue;
8738
+ this.updateMissionStatus(mission.id, mission.status, {
8739
+ operatorQueries: nextQueries
8740
+ }, [{
8741
+ at: closedAt,
8742
+ source: "system",
8743
+ text: `Auto-closed ${closedHere} stale operator query(ies): ${reason ?? "aged out"}.`,
8744
+ metadata: { closedCount: closedHere, reason: reason ?? "age" }
8745
+ }]);
8746
+ totalClosed += closedHere;
8747
+ missionsTouched += 1;
8748
+ breakdown.push({ missionId: mission.id, closed: closedHere, reason: reason ?? "age" });
8749
+ }
8750
+ return { closed: totalClosed, missionsTouched, breakdown };
8751
+ }
8681
8752
  // ─── Callback on disconnect (plan §7) ─────────────────
8682
8753
  /**
8683
8754
  * Flag a mission for callback-on-disconnect: the call dropped while
@@ -10690,7 +10761,7 @@ var SEARCH_EMAIL_TOOL = {
10690
10761
  var SEARCH_SKILLS_TOOL = {
10691
10762
  type: "function",
10692
10763
  name: "search_skills",
10693
- 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.",
10764
+ 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 up to 5 ranked matches, each with: id, name, BM25 score, when_to_use (the specific situation the skill is for), first_principle (the playbook\'s strategic frame), and a `recommendation` field. If `recommendation` says LOAD IT NOW or the top score > 0.3, immediately call load_skill with the top id \u2014 do not deliberate. If scores are weak (< 0.15), re-search with a different phrasing instead of loading a poor match. The model that judges "is this the right skill" is YOU \u2014 but the recommendation field will tell you what the right move usually is.',
10694
10765
  parameters: {
10695
10766
  type: "object",
10696
10767
  properties: {
package/dist/index.d.cts CHANGED
@@ -4109,6 +4109,37 @@ declare class PhoneManager {
4109
4109
  query: PhoneOperatorQuery;
4110
4110
  alreadyAnswered: boolean;
4111
4111
  } | null;
4112
+ /**
4113
+ * v0.9.92 — auto-close stale operator queries.
4114
+ *
4115
+ * An operator query is "stale" when:
4116
+ * - it's unanswered, AND
4117
+ * - either (a) the mission is no longer live (status in
4118
+ * {completed, failed, cancelled}), OR (b) the query is older
4119
+ * than `maxAgeSeconds`.
4120
+ *
4121
+ * The sweeper marks each stale query as answered with a synthetic
4122
+ * "[auto-closed: ...]" answer + `answeredVia: 'auto-sweeper'`. This
4123
+ * (a) gets the query out of `listOpenOperatorQueries`, (b) clears
4124
+ * the bridge's "you have N open questions" hint to the operator,
4125
+ * and (c) leaves a permanent audit trail of WHY the query was
4126
+ * closed.
4127
+ *
4128
+ * Returns the count of queries closed + a per-mission breakdown
4129
+ * for logging.
4130
+ */
4131
+ sweepStaleOperatorQueries(opts?: {
4132
+ maxAgeSeconds?: number;
4133
+ nowMs?: number;
4134
+ }): {
4135
+ closed: number;
4136
+ missionsTouched: number;
4137
+ breakdown: Array<{
4138
+ missionId: string;
4139
+ closed: number;
4140
+ reason: 'mission-terminal' | 'age';
4141
+ }>;
4142
+ };
4112
4143
  /**
4113
4144
  * Flag a mission for callback-on-disconnect: the call dropped while
4114
4145
  * an operator query was still unanswered, so once the operator
@@ -6010,6 +6041,32 @@ interface SkillSummary {
6010
6041
  version: string;
6011
6042
  disclaimer_required: boolean;
6012
6043
  estimated_call_duration_minutes: number;
6044
+ /**
6045
+ * v0.9.92 — surfaced from `context.when_to_use`. This is the field
6046
+ * that actually tells the model "should I load this for the
6047
+ * current situation?" — far more diagnostic than the generic
6048
+ * `description` (which is just a one-liner for browsing). The
6049
+ * realtime `search_skills` tool result includes it so the model
6050
+ * can decide WITHOUT a second `load_skill` round-trip on a wrong
6051
+ * guess.
6052
+ */
6053
+ when_to_use: string;
6054
+ /**
6055
+ * v0.9.92 — the skill's first principle, surfaced for the same
6056
+ * "is this the right playbook" decision. Principles are the
6057
+ * strategic frame ("be calm and friendly — the rep didn't choose
6058
+ * your bill"); seeing one principle tells the model whether the
6059
+ * skill's POSTURE matches the situation, not just its topic.
6060
+ */
6061
+ first_principle: string;
6062
+ /**
6063
+ * v0.9.92 — BM25F search score from the rank. Only present on
6064
+ * results from `searchSkills`; absent on `listSkills` output
6065
+ * (where the ordering is by id, not by relevance). Lets the
6066
+ * model thresholds "definitely load" vs. "re-search with a
6067
+ * better query".
6068
+ */
6069
+ score?: number;
6013
6070
  }
6014
6071
  /** Failed-validation result from the schema validator. */
6015
6072
  interface SkillValidationError {
package/dist/index.d.ts CHANGED
@@ -4109,6 +4109,37 @@ declare class PhoneManager {
4109
4109
  query: PhoneOperatorQuery;
4110
4110
  alreadyAnswered: boolean;
4111
4111
  } | null;
4112
+ /**
4113
+ * v0.9.92 — auto-close stale operator queries.
4114
+ *
4115
+ * An operator query is "stale" when:
4116
+ * - it's unanswered, AND
4117
+ * - either (a) the mission is no longer live (status in
4118
+ * {completed, failed, cancelled}), OR (b) the query is older
4119
+ * than `maxAgeSeconds`.
4120
+ *
4121
+ * The sweeper marks each stale query as answered with a synthetic
4122
+ * "[auto-closed: ...]" answer + `answeredVia: 'auto-sweeper'`. This
4123
+ * (a) gets the query out of `listOpenOperatorQueries`, (b) clears
4124
+ * the bridge's "you have N open questions" hint to the operator,
4125
+ * and (c) leaves a permanent audit trail of WHY the query was
4126
+ * closed.
4127
+ *
4128
+ * Returns the count of queries closed + a per-mission breakdown
4129
+ * for logging.
4130
+ */
4131
+ sweepStaleOperatorQueries(opts?: {
4132
+ maxAgeSeconds?: number;
4133
+ nowMs?: number;
4134
+ }): {
4135
+ closed: number;
4136
+ missionsTouched: number;
4137
+ breakdown: Array<{
4138
+ missionId: string;
4139
+ closed: number;
4140
+ reason: 'mission-terminal' | 'age';
4141
+ }>;
4142
+ };
4112
4143
  /**
4113
4144
  * Flag a mission for callback-on-disconnect: the call dropped while
4114
4145
  * an operator query was still unanswered, so once the operator
@@ -6010,6 +6041,32 @@ interface SkillSummary {
6010
6041
  version: string;
6011
6042
  disclaimer_required: boolean;
6012
6043
  estimated_call_duration_minutes: number;
6044
+ /**
6045
+ * v0.9.92 — surfaced from `context.when_to_use`. This is the field
6046
+ * that actually tells the model "should I load this for the
6047
+ * current situation?" — far more diagnostic than the generic
6048
+ * `description` (which is just a one-liner for browsing). The
6049
+ * realtime `search_skills` tool result includes it so the model
6050
+ * can decide WITHOUT a second `load_skill` round-trip on a wrong
6051
+ * guess.
6052
+ */
6053
+ when_to_use: string;
6054
+ /**
6055
+ * v0.9.92 — the skill's first principle, surfaced for the same
6056
+ * "is this the right playbook" decision. Principles are the
6057
+ * strategic frame ("be calm and friendly — the rep didn't choose
6058
+ * your bill"); seeing one principle tells the model whether the
6059
+ * skill's POSTURE matches the situation, not just its topic.
6060
+ */
6061
+ first_principle: string;
6062
+ /**
6063
+ * v0.9.92 — BM25F search score from the rank. Only present on
6064
+ * results from `searchSkills`; absent on `listSkills` output
6065
+ * (where the ordering is by id, not by relevance). Lets the
6066
+ * model thresholds "definitely load" vs. "re-search with a
6067
+ * better query".
6068
+ */
6069
+ score?: number;
6013
6070
  }
6014
6071
  /** Failed-validation result from the schema validator. */
6015
6072
  interface SkillValidationError {
package/dist/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  tokenize,
17
17
  userSkillsDir,
18
18
  validateSkill
19
- } from "./chunk-J6JINNJ3.js";
19
+ } from "./chunk-FYULLCZX.js";
20
20
  import {
21
21
  __require
22
22
  } from "./chunk-3RG5ZIWI.js";
@@ -7009,6 +7009,73 @@ var PhoneManager = class {
7009
7009
  }]);
7010
7010
  return { mission: updated, query: answered, alreadyAnswered: false };
7011
7011
  }
7012
+ /**
7013
+ * v0.9.92 — auto-close stale operator queries.
7014
+ *
7015
+ * An operator query is "stale" when:
7016
+ * - it's unanswered, AND
7017
+ * - either (a) the mission is no longer live (status in
7018
+ * {completed, failed, cancelled}), OR (b) the query is older
7019
+ * than `maxAgeSeconds`.
7020
+ *
7021
+ * The sweeper marks each stale query as answered with a synthetic
7022
+ * "[auto-closed: ...]" answer + `answeredVia: 'auto-sweeper'`. This
7023
+ * (a) gets the query out of `listOpenOperatorQueries`, (b) clears
7024
+ * the bridge's "you have N open questions" hint to the operator,
7025
+ * and (c) leaves a permanent audit trail of WHY the query was
7026
+ * closed.
7027
+ *
7028
+ * Returns the count of queries closed + a per-mission breakdown
7029
+ * for logging.
7030
+ */
7031
+ sweepStaleOperatorQueries(opts = {}) {
7032
+ const maxAgeMs = (opts.maxAgeSeconds ?? 3600) * 1e3;
7033
+ const nowMs = opts.nowMs ?? Date.now();
7034
+ const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
7035
+ const rows = this.db.prepare(
7036
+ `SELECT * FROM phone_missions WHERE metadata_json LIKE '%"operatorQueries"%'`
7037
+ ).all();
7038
+ const breakdown = [];
7039
+ let totalClosed = 0;
7040
+ let missionsTouched = 0;
7041
+ for (const row of rows) {
7042
+ const mission = rowToMission(row);
7043
+ const queries = readOperatorQueries(mission);
7044
+ const open = queries.filter((q) => !q.answer);
7045
+ if (open.length === 0) continue;
7046
+ const reason = TERMINAL.has(mission.status) ? "mission-terminal" : null;
7047
+ const closedAt = new Date(nowMs).toISOString();
7048
+ const nextQueries = queries.map((q) => {
7049
+ if (q.answer) return q;
7050
+ const askedAtMs = Date.parse(q.askedAt);
7051
+ const tooOld = Number.isFinite(askedAtMs) && nowMs - askedAtMs >= maxAgeMs;
7052
+ const queryReason = reason ?? (tooOld ? "age" : null);
7053
+ if (!queryReason) return q;
7054
+ return {
7055
+ ...q,
7056
+ answer: queryReason === "mission-terminal" ? `[auto-closed: mission ended (${mission.status}) before this question was answered]` : `[auto-closed: question went unanswered for over ${Math.round(maxAgeMs / 6e4)} minutes]`,
7057
+ answeredAt: closedAt,
7058
+ answeredVia: "auto-sweeper"
7059
+ };
7060
+ });
7061
+ const closedHere = nextQueries.filter(
7062
+ (q, i) => q.answer && !queries[i].answer && q.answeredVia === "auto-sweeper"
7063
+ ).length;
7064
+ if (closedHere === 0) continue;
7065
+ this.updateMissionStatus(mission.id, mission.status, {
7066
+ operatorQueries: nextQueries
7067
+ }, [{
7068
+ at: closedAt,
7069
+ source: "system",
7070
+ text: `Auto-closed ${closedHere} stale operator query(ies): ${reason ?? "aged out"}.`,
7071
+ metadata: { closedCount: closedHere, reason: reason ?? "age" }
7072
+ }]);
7073
+ totalClosed += closedHere;
7074
+ missionsTouched += 1;
7075
+ breakdown.push({ missionId: mission.id, closed: closedHere, reason: reason ?? "age" });
7076
+ }
7077
+ return { closed: totalClosed, missionsTouched, breakdown };
7078
+ }
7012
7079
  // ─── Callback on disconnect (plan §7) ─────────────────
7013
7080
  /**
7014
7081
  * Flag a mission for callback-on-disconnect: the call dropped while
@@ -9021,7 +9088,7 @@ var SEARCH_EMAIL_TOOL = {
9021
9088
  var SEARCH_SKILLS_TOOL = {
9022
9089
  type: "function",
9023
9090
  name: "search_skills",
9024
- 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.",
9091
+ 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 up to 5 ranked matches, each with: id, name, BM25 score, when_to_use (the specific situation the skill is for), first_principle (the playbook\'s strategic frame), and a `recommendation` field. If `recommendation` says LOAD IT NOW or the top score > 0.3, immediately call load_skill with the top id \u2014 do not deliberate. If scores are weak (< 0.15), re-search with a different phrasing instead of loading a poor match. The model that judges "is this the right skill" is YOU \u2014 but the recommendation field will tell you what the right move usually is.',
9025
9092
  parameters: {
9026
9093
  type: "object",
9027
9094
  properties: {
@@ -9676,7 +9743,7 @@ var RealtimeVoiceBridge = class {
9676
9743
  let loadSkill2;
9677
9744
  let renderSkillAsPrompt2;
9678
9745
  try {
9679
- ({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await import("./skills-RE3S767B.js"));
9746
+ ({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await import("./skills-DZVDIMTD.js"));
9680
9747
  } catch (err) {
9681
9748
  return { ok: false, message: `Skill registry unavailable: ${errorText(err)}` };
9682
9749
  }
@@ -8,7 +8,7 @@ import {
8
8
  skillFilename,
9
9
  userSkillsDir,
10
10
  validateSkill
11
- } from "./chunk-J6JINNJ3.js";
11
+ } from "./chunk-FYULLCZX.js";
12
12
  import "./chunk-3RG5ZIWI.js";
13
13
  export {
14
14
  invalidateSkillCache,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/core",
3
- "version": "0.9.37",
3
+ "version": "0.9.38",
4
4
  "description": "Core SDK for AgenticMail — email, SMS, and phone call-control for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",