@agenticmail/core 0.9.36 → 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
|
-
|
|
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
|
-
|
|
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:
|
|
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: {
|
|
@@ -11128,7 +11199,16 @@ function buildRealtimeSessionConfig(opts) {
|
|
|
11128
11199
|
audio: {
|
|
11129
11200
|
input: {
|
|
11130
11201
|
format: { ...audioFormat },
|
|
11131
|
-
turn_detection: { type: "server_vad" }
|
|
11202
|
+
turn_detection: { type: "server_vad" },
|
|
11203
|
+
// v0.9.91 — enable parallel transcription of the CALLER's audio
|
|
11204
|
+
// so the bridge can emit `provider`/`speaker:caller` transcript
|
|
11205
|
+
// entries. Without this opt-in OpenAI never sent
|
|
11206
|
+
// `conversation.item.input_audio_transcription.completed`
|
|
11207
|
+
// events, so the end-of-call digest only had the agent's side
|
|
11208
|
+
// of the conversation — half the call. `gpt-4o-mini-transcribe`
|
|
11209
|
+
// is the cheapest current Realtime-compatible transcription
|
|
11210
|
+
// model; falls back to whisper-1 server-side if unavailable.
|
|
11211
|
+
transcription: { model: "gpt-4o-mini-transcribe" }
|
|
11132
11212
|
},
|
|
11133
11213
|
output: {
|
|
11134
11214
|
format: { ...audioFormat },
|
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-
|
|
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:
|
|
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: {
|
|
@@ -9459,7 +9526,16 @@ function buildRealtimeSessionConfig(opts) {
|
|
|
9459
9526
|
audio: {
|
|
9460
9527
|
input: {
|
|
9461
9528
|
format: { ...audioFormat },
|
|
9462
|
-
turn_detection: { type: "server_vad" }
|
|
9529
|
+
turn_detection: { type: "server_vad" },
|
|
9530
|
+
// v0.9.91 — enable parallel transcription of the CALLER's audio
|
|
9531
|
+
// so the bridge can emit `provider`/`speaker:caller` transcript
|
|
9532
|
+
// entries. Without this opt-in OpenAI never sent
|
|
9533
|
+
// `conversation.item.input_audio_transcription.completed`
|
|
9534
|
+
// events, so the end-of-call digest only had the agent's side
|
|
9535
|
+
// of the conversation — half the call. `gpt-4o-mini-transcribe`
|
|
9536
|
+
// is the cheapest current Realtime-compatible transcription
|
|
9537
|
+
// model; falls back to whisper-1 server-side if unavailable.
|
|
9538
|
+
transcription: { model: "gpt-4o-mini-transcribe" }
|
|
9463
9539
|
},
|
|
9464
9540
|
output: {
|
|
9465
9541
|
format: { ...audioFormat },
|
|
@@ -9667,7 +9743,7 @@ var RealtimeVoiceBridge = class {
|
|
|
9667
9743
|
let loadSkill2;
|
|
9668
9744
|
let renderSkillAsPrompt2;
|
|
9669
9745
|
try {
|
|
9670
|
-
({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await import("./skills-
|
|
9746
|
+
({ loadSkill: loadSkill2, renderSkillAsPrompt: renderSkillAsPrompt2 } = await import("./skills-DZVDIMTD.js"));
|
|
9671
9747
|
} catch (err) {
|
|
9672
9748
|
return { ok: false, message: `Skill registry unavailable: ${errorText(err)}` };
|
|
9673
9749
|
}
|