getpatter 0.6.5 → 0.6.6
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-CRPJLVHB.mjs → chunk-YJX2EKON.mjs} +649 -80
- package/dist/cli.js +492 -2
- package/dist/index.d.mts +233 -10
- package/dist/index.d.ts +233 -10
- package/dist/index.js +1572 -198
- package/dist/index.mjs +864 -78
- package/dist/{test-mode-HGHI2AUV.mjs → test-mode-XFOADUNE.mjs} +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,10 +5,10 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __glob = (map) => (
|
|
9
|
-
var fn = map[
|
|
8
|
+
var __glob = (map) => (path7) => {
|
|
9
|
+
var fn = map[path7];
|
|
10
10
|
if (fn) return fn();
|
|
11
|
-
throw new Error("Module not found in bundle: " +
|
|
11
|
+
throw new Error("Module not found in bundle: " + path7);
|
|
12
12
|
};
|
|
13
13
|
var __esm = (fn, res) => function __init() {
|
|
14
14
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
@@ -118,6 +118,110 @@ var init_errors = __esm({
|
|
|
118
118
|
}
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
// src/telemetry/call-metrics.ts
|
|
122
|
+
function engineFromMode(mode) {
|
|
123
|
+
if (mode === "openai_realtime" || mode === "openai_realtime_2") return "realtime";
|
|
124
|
+
if (mode === "elevenlabs_convai") return "convai";
|
|
125
|
+
if (mode === "pipeline") return "pipeline";
|
|
126
|
+
return "other";
|
|
127
|
+
}
|
|
128
|
+
function providerFromMetrics(m) {
|
|
129
|
+
const mode = m.provider_mode;
|
|
130
|
+
if (mode === "openai_realtime" || mode === "openai_realtime_2") return "openai";
|
|
131
|
+
if (mode === "elevenlabs_convai") return "elevenlabs";
|
|
132
|
+
for (const key of ["llm_provider", "stt_provider", "tts_provider"]) {
|
|
133
|
+
const v = m[key];
|
|
134
|
+
if (typeof v === "string" && v) return v.toLowerCase();
|
|
135
|
+
}
|
|
136
|
+
return "other";
|
|
137
|
+
}
|
|
138
|
+
function providerFromMode(mode) {
|
|
139
|
+
if (mode === "openai_realtime" || mode === "openai_realtime_2") return "openai";
|
|
140
|
+
if (mode === "elevenlabs_convai") return "elevenlabs";
|
|
141
|
+
return "other";
|
|
142
|
+
}
|
|
143
|
+
function carrierFamily(tp) {
|
|
144
|
+
return typeof tp === "string" && tp ? tp.toLowerCase() : "none";
|
|
145
|
+
}
|
|
146
|
+
function direction(value) {
|
|
147
|
+
const v = typeof value === "string" ? value.toLowerCase() : "";
|
|
148
|
+
return v === "inbound" || v === "outbound" ? v : void 0;
|
|
149
|
+
}
|
|
150
|
+
function turnCountBucket(n) {
|
|
151
|
+
if (n <= 0) return "0";
|
|
152
|
+
if (n === 1) return "1";
|
|
153
|
+
if (n <= 3) return "2_3";
|
|
154
|
+
if (n <= 6) return "4_6";
|
|
155
|
+
if (n <= 12) return "7_12";
|
|
156
|
+
return "13_plus";
|
|
157
|
+
}
|
|
158
|
+
function latencyMs(m) {
|
|
159
|
+
const p95 = m.latency_p95;
|
|
160
|
+
if (p95 && typeof p95 === "object") {
|
|
161
|
+
return p95.agent_response_ms;
|
|
162
|
+
}
|
|
163
|
+
return void 0;
|
|
164
|
+
}
|
|
165
|
+
function recordCallStarted(telemetry, opts) {
|
|
166
|
+
if (!telemetry) return;
|
|
167
|
+
try {
|
|
168
|
+
const dims = {
|
|
169
|
+
engine: engineFromMode(opts.providerMode),
|
|
170
|
+
provider: providerFromMode(opts.providerMode),
|
|
171
|
+
carrier: carrierFamily(opts.telephonyProvider)
|
|
172
|
+
};
|
|
173
|
+
const d = direction(opts.direction);
|
|
174
|
+
if (d !== void 0) dims.direction = d;
|
|
175
|
+
telemetry.record("call_started", dims);
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function recordCallCompleted(telemetry, opts) {
|
|
180
|
+
if (!telemetry) return;
|
|
181
|
+
try {
|
|
182
|
+
const dims = { outcome: opts.outcome };
|
|
183
|
+
const d = direction(opts.direction);
|
|
184
|
+
if (d !== void 0) dims.direction = d;
|
|
185
|
+
const metrics = opts.metrics;
|
|
186
|
+
if (metrics && typeof metrics === "object") {
|
|
187
|
+
const m = metrics;
|
|
188
|
+
dims.engine = engineFromMode(m.provider_mode);
|
|
189
|
+
dims.provider = providerFromMetrics(m);
|
|
190
|
+
dims.carrier = carrierFamily(m.telephony_provider);
|
|
191
|
+
if (typeof m.duration_seconds === "number") {
|
|
192
|
+
dims.duration_seconds = Math.max(0, Math.round(m.duration_seconds));
|
|
193
|
+
}
|
|
194
|
+
const lat = latencyMs(m);
|
|
195
|
+
if (typeof lat === "number") dims.latency_ms = Math.max(0, Math.round(lat));
|
|
196
|
+
const cost = m.cost;
|
|
197
|
+
if (cost && typeof cost === "object") {
|
|
198
|
+
const total = cost.total;
|
|
199
|
+
if (typeof total === "number" && Number.isFinite(total)) {
|
|
200
|
+
dims.cost_usd = Math.max(0, Math.round(total * 1e4) / 1e4);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (Array.isArray(m.turns)) {
|
|
204
|
+
dims.turn_count_bucket = turnCountBucket(m.turns.length);
|
|
205
|
+
}
|
|
206
|
+
const errorCode = m.error_code;
|
|
207
|
+
if (typeof errorCode === "string" && errorCode) {
|
|
208
|
+
dims.error_code = errorCode;
|
|
209
|
+
dims.outcome = "error";
|
|
210
|
+
}
|
|
211
|
+
} else if (opts.carrier !== void 0) {
|
|
212
|
+
dims.carrier = carrierFamily(opts.carrier);
|
|
213
|
+
}
|
|
214
|
+
telemetry.record("call_completed", dims);
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
var init_call_metrics = __esm({
|
|
219
|
+
"src/telemetry/call-metrics.ts"() {
|
|
220
|
+
"use strict";
|
|
221
|
+
init_cjs_shims();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
121
225
|
// src/logger.ts
|
|
122
226
|
function getLogger() {
|
|
123
227
|
return currentLogger;
|
|
@@ -2171,10 +2275,10 @@ var init_plivo_adapter = __esm({
|
|
|
2171
2275
|
this.baseUrl = `${PLIVO_API_BASE}/Account/${encodeURIComponent(authId)}`;
|
|
2172
2276
|
this.authHeader = `Basic ${Buffer.from(`${authId}:${authToken}`).toString("base64")}`;
|
|
2173
2277
|
}
|
|
2174
|
-
async request(method,
|
|
2278
|
+
async request(method, path7, jsonBody) {
|
|
2175
2279
|
const headers = { Authorization: this.authHeader };
|
|
2176
2280
|
if (jsonBody !== void 0) headers["Content-Type"] = "application/json";
|
|
2177
|
-
const response = await fetch(`${this.baseUrl}${
|
|
2281
|
+
const response = await fetch(`${this.baseUrl}${path7}`, {
|
|
2178
2282
|
method,
|
|
2179
2283
|
headers,
|
|
2180
2284
|
body: jsonBody !== void 0 ? JSON.stringify(jsonBody) : void 0,
|
|
@@ -2182,7 +2286,7 @@ var init_plivo_adapter = __esm({
|
|
|
2182
2286
|
});
|
|
2183
2287
|
const text = await response.text();
|
|
2184
2288
|
if (!response.ok && response.status !== 404) {
|
|
2185
|
-
throw new Error(`Plivo ${method} ${
|
|
2289
|
+
throw new Error(`Plivo ${method} ${path7} failed: ${response.status} ${text}`);
|
|
2186
2290
|
}
|
|
2187
2291
|
let data = {};
|
|
2188
2292
|
if (text) {
|
|
@@ -3705,9 +3809,9 @@ function loadDashboardHtml() {
|
|
|
3705
3809
|
(0, import_node_path.join)(here, "dashboard", "ui.html"),
|
|
3706
3810
|
(0, import_node_path.join)(here, "..", "dashboard", "ui.html")
|
|
3707
3811
|
];
|
|
3708
|
-
for (const
|
|
3812
|
+
for (const path7 of candidates) {
|
|
3709
3813
|
try {
|
|
3710
|
-
return (0, import_node_fs.readFileSync)(
|
|
3814
|
+
return (0, import_node_fs.readFileSync)(path7, "utf8");
|
|
3711
3815
|
} catch {
|
|
3712
3816
|
}
|
|
3713
3817
|
}
|
|
@@ -4599,6 +4703,9 @@ var init_metrics = __esm({
|
|
|
4599
4703
|
ttsModel;
|
|
4600
4704
|
realtimeModel;
|
|
4601
4705
|
_pricing;
|
|
4706
|
+
// Terminal error code (lowercased ErrorCode value or "other"); set by
|
|
4707
|
+
// recordError when the call ends abnormally. Empty for a clean call.
|
|
4708
|
+
_errorCode = "";
|
|
4602
4709
|
_callStart;
|
|
4603
4710
|
_turns = [];
|
|
4604
4711
|
// mutable internal array; immutable when exposed via TurnMetrics[] → readonly TurnMetrics[]
|
|
@@ -5169,11 +5276,35 @@ var init_metrics = __esm({
|
|
|
5169
5276
|
telephony_provider: this.telephonyProvider,
|
|
5170
5277
|
stt_model: this.sttModel,
|
|
5171
5278
|
tts_model: this.ttsModel,
|
|
5172
|
-
llm_model: this._llmModel
|
|
5279
|
+
llm_model: this._llmModel,
|
|
5280
|
+
error_code: this._errorCode
|
|
5173
5281
|
};
|
|
5174
5282
|
this._eventBus?.emit("call_ended", { callId: this.callId, metrics });
|
|
5175
5283
|
return metrics;
|
|
5176
5284
|
}
|
|
5285
|
+
/**
|
|
5286
|
+
* Record the call's terminal error as a coarse, anonymous code. Stores the
|
|
5287
|
+
* PatterError `.code` lowercased; maps common timeout/connection errors; falls
|
|
5288
|
+
* back to "other". Never stores the message. Last write wins.
|
|
5289
|
+
*/
|
|
5290
|
+
recordError(err) {
|
|
5291
|
+
const code = err?.code;
|
|
5292
|
+
const name = err?.name;
|
|
5293
|
+
const sys = typeof code === "string" ? code : "";
|
|
5294
|
+
if (sys.startsWith("ECONN") || sys === "EHOSTUNREACH" || sys === "ENETUNREACH" || sys === "EPIPE") {
|
|
5295
|
+
this._errorCode = "connection";
|
|
5296
|
+
return;
|
|
5297
|
+
}
|
|
5298
|
+
if (typeof code === "string" && code) {
|
|
5299
|
+
this._errorCode = code.toLowerCase();
|
|
5300
|
+
return;
|
|
5301
|
+
}
|
|
5302
|
+
if (name === "TimeoutError" || name === "AbortError") {
|
|
5303
|
+
this._errorCode = "timeout";
|
|
5304
|
+
} else {
|
|
5305
|
+
this._errorCode = "other";
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5177
5308
|
/** Return the cost breakdown for the call so far without ending it. */
|
|
5178
5309
|
getCostSoFar() {
|
|
5179
5310
|
const duration3 = (hrTimeMs() - this._callStart) / 1e3;
|
|
@@ -5963,7 +6094,7 @@ var init_llm_loop = __esm({
|
|
|
5963
6094
|
});
|
|
5964
6095
|
if (!response.ok) {
|
|
5965
6096
|
const errText = await response.text();
|
|
5966
|
-
getLogger().error(`LLM API error: ${response.status} ${errText}`);
|
|
6097
|
+
getLogger().error(`LLM API error: ${response.status} ${errText.slice(0, 200)}`);
|
|
5967
6098
|
throw new PatterConnectionError(
|
|
5968
6099
|
`LLM API returned ${response.status}: ${errText.slice(0, 200)}`
|
|
5969
6100
|
);
|
|
@@ -6132,7 +6263,15 @@ ${systemPrompt}` : DEFAULT_PHONE_PREAMBLE;
|
|
|
6132
6263
|
const hasAfterLlmChunk = Boolean(hookExecutor?.hasAfterLlmChunk());
|
|
6133
6264
|
const allEmittedText = [];
|
|
6134
6265
|
const callId = callContext.call_id;
|
|
6135
|
-
const
|
|
6266
|
+
const caller = callContext.caller;
|
|
6267
|
+
const callee = callContext.callee;
|
|
6268
|
+
const hasContext = typeof callId === "string" && callId.length > 0 || typeof caller === "string" && caller.length > 0 || typeof callee === "string" && callee.length > 0;
|
|
6269
|
+
const streamOpts = hasContext ? {
|
|
6270
|
+
...opts,
|
|
6271
|
+
...typeof callId === "string" && callId.length > 0 ? { callId } : {},
|
|
6272
|
+
...typeof caller === "string" && caller.length > 0 ? { caller } : {},
|
|
6273
|
+
...typeof callee === "string" && callee.length > 0 ? { callee } : {}
|
|
6274
|
+
} : opts;
|
|
6136
6275
|
for (let iter = 0; iter < maxIterations; iter++) {
|
|
6137
6276
|
const toolCallsAccumulated = /* @__PURE__ */ new Map();
|
|
6138
6277
|
const textParts = [];
|
|
@@ -6266,6 +6405,7 @@ ${systemPrompt}` : DEFAULT_PHONE_PREAMBLE;
|
|
|
6266
6405
|
{ role: "system", content: this.systemPrompt }
|
|
6267
6406
|
];
|
|
6268
6407
|
for (const entry of history) {
|
|
6408
|
+
if (entry.role === "tool") continue;
|
|
6269
6409
|
messages.push({
|
|
6270
6410
|
role: entry.role === "assistant" ? "assistant" : "user",
|
|
6271
6411
|
content: entry.text
|
|
@@ -6543,10 +6683,10 @@ function mergeDefs(...defs) {
|
|
|
6543
6683
|
function cloneDef(schema) {
|
|
6544
6684
|
return mergeDefs(schema._zod.def);
|
|
6545
6685
|
}
|
|
6546
|
-
function getElementAtPath(obj,
|
|
6547
|
-
if (!
|
|
6686
|
+
function getElementAtPath(obj, path7) {
|
|
6687
|
+
if (!path7)
|
|
6548
6688
|
return obj;
|
|
6549
|
-
return
|
|
6689
|
+
return path7.reduce((acc, key) => acc?.[key], obj);
|
|
6550
6690
|
}
|
|
6551
6691
|
function promiseAllObject(promisesObj) {
|
|
6552
6692
|
const keys = Object.keys(promisesObj);
|
|
@@ -6874,11 +7014,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
6874
7014
|
}
|
|
6875
7015
|
return false;
|
|
6876
7016
|
}
|
|
6877
|
-
function prefixIssues(
|
|
7017
|
+
function prefixIssues(path7, issues) {
|
|
6878
7018
|
return issues.map((iss) => {
|
|
6879
7019
|
var _a3;
|
|
6880
7020
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
6881
|
-
iss.path.unshift(
|
|
7021
|
+
iss.path.unshift(path7);
|
|
6882
7022
|
return iss;
|
|
6883
7023
|
});
|
|
6884
7024
|
}
|
|
@@ -7097,16 +7237,16 @@ function flattenError(error2, mapper = (issue2) => issue2.message) {
|
|
|
7097
7237
|
}
|
|
7098
7238
|
function formatError(error2, mapper = (issue2) => issue2.message) {
|
|
7099
7239
|
const fieldErrors = { _errors: [] };
|
|
7100
|
-
const processError = (error3,
|
|
7240
|
+
const processError = (error3, path7 = []) => {
|
|
7101
7241
|
for (const issue2 of error3.issues) {
|
|
7102
7242
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
7103
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
7243
|
+
issue2.errors.map((issues) => processError({ issues }, [...path7, ...issue2.path]));
|
|
7104
7244
|
} else if (issue2.code === "invalid_key") {
|
|
7105
|
-
processError({ issues: issue2.issues }, [...
|
|
7245
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
7106
7246
|
} else if (issue2.code === "invalid_element") {
|
|
7107
|
-
processError({ issues: issue2.issues }, [...
|
|
7247
|
+
processError({ issues: issue2.issues }, [...path7, ...issue2.path]);
|
|
7108
7248
|
} else {
|
|
7109
|
-
const fullpath = [...
|
|
7249
|
+
const fullpath = [...path7, ...issue2.path];
|
|
7110
7250
|
if (fullpath.length === 0) {
|
|
7111
7251
|
fieldErrors._errors.push(mapper(issue2));
|
|
7112
7252
|
} else {
|
|
@@ -17910,20 +18050,20 @@ var require_compile = __commonJS({
|
|
|
17910
18050
|
var util_1 = require_util();
|
|
17911
18051
|
var validate_1 = require_validate();
|
|
17912
18052
|
var SchemaEnv = class {
|
|
17913
|
-
constructor(
|
|
18053
|
+
constructor(env2) {
|
|
17914
18054
|
var _a3;
|
|
17915
18055
|
this.refs = {};
|
|
17916
18056
|
this.dynamicAnchors = {};
|
|
17917
18057
|
let schema;
|
|
17918
|
-
if (typeof
|
|
17919
|
-
schema =
|
|
17920
|
-
this.schema =
|
|
17921
|
-
this.schemaId =
|
|
17922
|
-
this.root =
|
|
17923
|
-
this.baseId = (_a3 =
|
|
17924
|
-
this.schemaPath =
|
|
17925
|
-
this.localRefs =
|
|
17926
|
-
this.meta =
|
|
18058
|
+
if (typeof env2.schema == "object")
|
|
18059
|
+
schema = env2.schema;
|
|
18060
|
+
this.schema = env2.schema;
|
|
18061
|
+
this.schemaId = env2.schemaId;
|
|
18062
|
+
this.root = env2.root || this;
|
|
18063
|
+
this.baseId = (_a3 = env2.baseId) !== null && _a3 !== void 0 ? _a3 : (0, resolve_1.normalizeId)(schema === null || schema === void 0 ? void 0 : schema[env2.schemaId || "$id"]);
|
|
18064
|
+
this.schemaPath = env2.schemaPath;
|
|
18065
|
+
this.localRefs = env2.localRefs;
|
|
18066
|
+
this.meta = env2.meta;
|
|
17927
18067
|
this.$async = schema === null || schema === void 0 ? void 0 : schema.$async;
|
|
17928
18068
|
this.refs = {};
|
|
17929
18069
|
}
|
|
@@ -18107,15 +18247,15 @@ var require_compile = __commonJS({
|
|
|
18107
18247
|
baseId = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schId);
|
|
18108
18248
|
}
|
|
18109
18249
|
}
|
|
18110
|
-
let
|
|
18250
|
+
let env2;
|
|
18111
18251
|
if (typeof schema != "boolean" && schema.$ref && !(0, util_1.schemaHasRulesButRef)(schema, this.RULES)) {
|
|
18112
18252
|
const $ref = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, schema.$ref);
|
|
18113
|
-
|
|
18253
|
+
env2 = resolveSchema.call(this, root, $ref);
|
|
18114
18254
|
}
|
|
18115
18255
|
const { schemaId } = this.opts;
|
|
18116
|
-
|
|
18117
|
-
if (
|
|
18118
|
-
return
|
|
18256
|
+
env2 = env2 || new SchemaEnv({ schema, schemaId, root, baseId });
|
|
18257
|
+
if (env2.schema !== env2.root.schema)
|
|
18258
|
+
return env2;
|
|
18119
18259
|
return void 0;
|
|
18120
18260
|
}
|
|
18121
18261
|
}
|
|
@@ -18267,8 +18407,8 @@ var require_utils = __commonJS({
|
|
|
18267
18407
|
}
|
|
18268
18408
|
return ind;
|
|
18269
18409
|
}
|
|
18270
|
-
function removeDotSegments(
|
|
18271
|
-
let input =
|
|
18410
|
+
function removeDotSegments(path7) {
|
|
18411
|
+
let input = path7;
|
|
18272
18412
|
const output = [];
|
|
18273
18413
|
let nextSlash = -1;
|
|
18274
18414
|
let len = 0;
|
|
@@ -18521,8 +18661,8 @@ var require_schemes = __commonJS({
|
|
|
18521
18661
|
wsComponent.secure = void 0;
|
|
18522
18662
|
}
|
|
18523
18663
|
if (wsComponent.resourceName) {
|
|
18524
|
-
const [
|
|
18525
|
-
wsComponent.path =
|
|
18664
|
+
const [path7, query] = wsComponent.resourceName.split("?");
|
|
18665
|
+
wsComponent.path = path7 && path7 !== "/" ? path7 : void 0;
|
|
18526
18666
|
wsComponent.query = query;
|
|
18527
18667
|
wsComponent.resourceName = void 0;
|
|
18528
18668
|
}
|
|
@@ -19610,8 +19750,8 @@ var require_ref = __commonJS({
|
|
|
19610
19750
|
schemaType: "string",
|
|
19611
19751
|
code(cxt) {
|
|
19612
19752
|
const { gen, schema: $ref, it } = cxt;
|
|
19613
|
-
const { baseId, schemaEnv:
|
|
19614
|
-
const { root } =
|
|
19753
|
+
const { baseId, schemaEnv: env2, validateName, opts, self } = it;
|
|
19754
|
+
const { root } = env2;
|
|
19615
19755
|
if (($ref === "#" || $ref === "#/") && baseId === root.baseId)
|
|
19616
19756
|
return callRootRef();
|
|
19617
19757
|
const schOrEnv = compile_1.resolveRef.call(self, root, baseId, $ref);
|
|
@@ -19621,8 +19761,8 @@ var require_ref = __commonJS({
|
|
|
19621
19761
|
return callValidate(schOrEnv);
|
|
19622
19762
|
return inlineRefSchema(schOrEnv);
|
|
19623
19763
|
function callRootRef() {
|
|
19624
|
-
if (
|
|
19625
|
-
return callRef(cxt, validateName,
|
|
19764
|
+
if (env2 === root)
|
|
19765
|
+
return callRef(cxt, validateName, env2, env2.$async);
|
|
19626
19766
|
const rootName = gen.scopeValue("root", { ref: root });
|
|
19627
19767
|
return callRef(cxt, (0, codegen_1._)`${rootName}.validate`, root, root.$async);
|
|
19628
19768
|
}
|
|
@@ -19652,14 +19792,14 @@ var require_ref = __commonJS({
|
|
|
19652
19792
|
exports2.getValidate = getValidate;
|
|
19653
19793
|
function callRef(cxt, v, sch, $async) {
|
|
19654
19794
|
const { gen, it } = cxt;
|
|
19655
|
-
const { allErrors, schemaEnv:
|
|
19795
|
+
const { allErrors, schemaEnv: env2, opts } = it;
|
|
19656
19796
|
const passCxt = opts.passContext ? names_1.default.this : codegen_1.nil;
|
|
19657
19797
|
if ($async)
|
|
19658
19798
|
callAsyncRef();
|
|
19659
19799
|
else
|
|
19660
19800
|
callSyncRef();
|
|
19661
19801
|
function callAsyncRef() {
|
|
19662
|
-
if (!
|
|
19802
|
+
if (!env2.$async)
|
|
19663
19803
|
throw new Error("async schema referenced by sync schema");
|
|
19664
19804
|
const valid = gen.let("valid");
|
|
19665
19805
|
gen.try(() => {
|
|
@@ -21961,12 +22101,12 @@ var require_dist = __commonJS({
|
|
|
21961
22101
|
throw new Error(`Unknown format "${name}"`);
|
|
21962
22102
|
return f;
|
|
21963
22103
|
};
|
|
21964
|
-
function addFormats(ajv, list,
|
|
22104
|
+
function addFormats(ajv, list, fs8, exportName) {
|
|
21965
22105
|
var _a3;
|
|
21966
22106
|
var _b;
|
|
21967
22107
|
(_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
|
|
21968
22108
|
for (const f of list)
|
|
21969
|
-
ajv.addFormat(f,
|
|
22109
|
+
ajv.addFormat(f, fs8[f]);
|
|
21970
22110
|
}
|
|
21971
22111
|
module2.exports = exports2 = formatsPlugin;
|
|
21972
22112
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
@@ -27783,6 +27923,26 @@ function isSttHallucination(text) {
|
|
|
27783
27923
|
const pieces = stripped.split(/[.!?…。!?]+/u).map((p) => p.trim()).filter((p) => p.length > 0);
|
|
27784
27924
|
return pieces.length > 1 && pieces.every((p) => HALLUCINATIONS.has(p));
|
|
27785
27925
|
}
|
|
27926
|
+
function normalizeForEcho(text) {
|
|
27927
|
+
return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/u, " ").trim().replace(/\s+/gu, " ");
|
|
27928
|
+
}
|
|
27929
|
+
function looksLikeEcho(candidate, agentText) {
|
|
27930
|
+
const a = normalizeForEcho(agentText);
|
|
27931
|
+
const c = normalizeForEcho(candidate);
|
|
27932
|
+
if (!a || !c) return false;
|
|
27933
|
+
const words = c.split(" ").filter(Boolean);
|
|
27934
|
+
if (words.length < ECHO_MIN_CANDIDATE_WORDS) return false;
|
|
27935
|
+
if (a.includes(c)) return true;
|
|
27936
|
+
const agentWords = new Set(a.split(" "));
|
|
27937
|
+
const overlap = words.filter((w) => agentWords.has(w)).length / words.length;
|
|
27938
|
+
return overlap >= ECHO_WORD_OVERLAP_THRESHOLD;
|
|
27939
|
+
}
|
|
27940
|
+
function isNearDuplicate(a, b) {
|
|
27941
|
+
if (!a || !b) return false;
|
|
27942
|
+
if (a === b) return true;
|
|
27943
|
+
const [shorter, longer] = a.length <= b.length ? [a, b] : [b, a];
|
|
27944
|
+
return longer.startsWith(shorter + " ");
|
|
27945
|
+
}
|
|
27786
27946
|
async function queryDeepgramCost(metricsAcc, deepgramKey, deepgramRequestId) {
|
|
27787
27947
|
try {
|
|
27788
27948
|
const projResp = await fetch("https://api.deepgram.com/v1/projects", {
|
|
@@ -27813,7 +27973,7 @@ async function queryDeepgramCost(metricsAcc, deepgramKey, deepgramRequestId) {
|
|
|
27813
27973
|
} catch {
|
|
27814
27974
|
}
|
|
27815
27975
|
}
|
|
27816
|
-
var DEFAULT_TOOL_CALL_PREAMBLE_BLOCK, HALLUCINATIONS, StreamHandler;
|
|
27976
|
+
var DEFAULT_TOOL_CALL_PREAMBLE_BLOCK, HALLUCINATIONS, ECHO_WORD_OVERLAP_THRESHOLD, ECHO_MIN_CANDIDATE_WORDS, StreamHandler;
|
|
27817
27977
|
var init_stream_handler = __esm({
|
|
27818
27978
|
"src/stream-handler.ts"() {
|
|
27819
27979
|
"use strict";
|
|
@@ -27926,6 +28086,8 @@ Avoid:
|
|
|
27926
28086
|
"[blank_audio]",
|
|
27927
28087
|
"(silence)"
|
|
27928
28088
|
]);
|
|
28089
|
+
ECHO_WORD_OVERLAP_THRESHOLD = 0.6;
|
|
28090
|
+
ECHO_MIN_CANDIDATE_WORDS = 4;
|
|
27929
28091
|
StreamHandler = class _StreamHandler {
|
|
27930
28092
|
deps;
|
|
27931
28093
|
ws;
|
|
@@ -27938,6 +28100,17 @@ Avoid:
|
|
|
27938
28100
|
stt = null;
|
|
27939
28101
|
tts = null;
|
|
27940
28102
|
isSpeaking = false;
|
|
28103
|
+
/**
|
|
28104
|
+
* True only while the post-TTS tail-grace window is pending: the agent has
|
|
28105
|
+
* finished its turn but ``isSpeaking`` is still held for
|
|
28106
|
+
* ``PATTER_TTS_TAIL_GRACE_MS`` to swallow the fading echo tail. A VAD
|
|
28107
|
+
* ``speech_start`` (or a transcript) during this window is the user's NEXT
|
|
28108
|
+
* turn, not a barge-in — there is nothing left to interrupt. Set by
|
|
28109
|
+
* ``endSpeakingWithGrace``; cleared by ``beginSpeaking``, the grace flip,
|
|
28110
|
+
* ``cancelSpeaking``, and ``endTailGraceForNewTurn``. Parity with Python
|
|
28111
|
+
* ``_tail_grace_active``.
|
|
28112
|
+
*/
|
|
28113
|
+
tailGraceActive = false;
|
|
27941
28114
|
/**
|
|
27942
28115
|
* Ring buffer of inbound PCM16 16 kHz frames captured while the agent
|
|
27943
28116
|
* is speaking and the self-hearing guard is dropping audio. On
|
|
@@ -28013,6 +28186,35 @@ Avoid:
|
|
|
28013
28186
|
* ``isSpeaking=false``, and silently cut the agent's first turn.
|
|
28014
28187
|
*/
|
|
28015
28188
|
firstAudioSentAt = null;
|
|
28189
|
+
/**
|
|
28190
|
+
* Estimated wall-clock (ms) when the LAST audio byte pushed to the carrier
|
|
28191
|
+
* finishes PLAYING on the phone. The pipeline pushes TTS audio as fast as
|
|
28192
|
+
* the provider synthesizes it (no pacing) and the carrier buffers + plays
|
|
28193
|
+
* at realtime, so "we finished pushing" and "the caller finished hearing"
|
|
28194
|
+
* can diverge by tens of seconds — especially with agent-runtime LLMs
|
|
28195
|
+
* (Hermes/OpenClaw) that deliver a long reply all at once after a thinking
|
|
28196
|
+
* pause. ``endSpeakingWithGrace`` holds ``isSpeaking=true`` (with
|
|
28197
|
+
* ``tailGraceActive=false``) until this cursor passes, so a barge-in during
|
|
28198
|
+
* the audible backlog still takes the cancel path (``sendClear`` drops the
|
|
28199
|
+
* carrier buffer) instead of being treated as a calm next turn. Advanced by
|
|
28200
|
+
* ``trackOutboundPlayback``; reset by ``cancelSpeaking`` (the buffer is
|
|
28201
|
+
* cleared) and ``endTailGraceForNewTurn``.
|
|
28202
|
+
*/
|
|
28203
|
+
playbackBufferedUntil = 0;
|
|
28204
|
+
/**
|
|
28205
|
+
* Per-turn playback timeline used to estimate the response prefix the
|
|
28206
|
+
* caller actually HEARD when a barge-in lands. ``turnPlaybackTotalMs``
|
|
28207
|
+
* accumulates the playout duration of every chunk pushed this turn
|
|
28208
|
+
* (including filler audio, which keeps the timeline aligned);
|
|
28209
|
+
* ``turnSpokenSegments`` records ``{text, startMs}`` for each RESPONSE
|
|
28210
|
+
* sentence at its first audible chunk (filler / error-fallback audio
|
|
28211
|
+
* advances the clock but adds no segment). ``heard = total - backlog``
|
|
28212
|
+
* then maps to a sentence-granular prefix — see ``heardResponsePrefix``.
|
|
28213
|
+
* Both reset at ``beginSpeaking``. Mirrors Python
|
|
28214
|
+
* ``_turn_playback_total_s`` / ``_turn_spoken_segments``.
|
|
28215
|
+
*/
|
|
28216
|
+
turnPlaybackTotalMs = 0;
|
|
28217
|
+
turnSpokenSegments = [];
|
|
28016
28218
|
/**
|
|
28017
28219
|
* Optional barge-in confirmation strategies. With an empty array the
|
|
28018
28220
|
* SDK falls back to the legacy "cancel on first VAD speech_start"
|
|
@@ -28130,11 +28332,15 @@ Avoid:
|
|
|
28130
28332
|
}
|
|
28131
28333
|
this.speakingGeneration++;
|
|
28132
28334
|
this.isSpeaking = true;
|
|
28335
|
+
this.tailGraceActive = false;
|
|
28133
28336
|
this.speakingStartedAt = Date.now();
|
|
28134
28337
|
this.suppressedSpeechPending = false;
|
|
28135
28338
|
void isFirstMessage;
|
|
28136
28339
|
this.firstAudioSentAt = Date.now();
|
|
28137
28340
|
this.inboundAudioRing = [];
|
|
28341
|
+
this.currentAgentSpokenText = "";
|
|
28342
|
+
this.turnPlaybackTotalMs = 0;
|
|
28343
|
+
this.turnSpokenSegments = [];
|
|
28138
28344
|
this.resetVad();
|
|
28139
28345
|
}
|
|
28140
28346
|
/**
|
|
@@ -28149,6 +28355,87 @@ Avoid:
|
|
|
28149
28355
|
this.firstAudioSentAt = Date.now();
|
|
28150
28356
|
}
|
|
28151
28357
|
}
|
|
28358
|
+
/**
|
|
28359
|
+
* Advance ``playbackBufferedUntil`` by the playout duration of an outbound
|
|
28360
|
+
* TTS chunk. ``numBytes`` is the size of the chunk BEFORE carrier encoding
|
|
28361
|
+
* (the same buffer handed to ``encodePipelineAudio``): PCM16 @ 16 kHz in
|
|
28362
|
+
* the default path (32 bytes/ms), or the carrier's native μ-law @ 8 kHz
|
|
28363
|
+
* (8 bytes/ms) when the TTS adapter emits wire format directly
|
|
28364
|
+
* (``ttsOutputFormatNativeForCarrier`` — Twilio/Plivo ``ulaw_8000``;
|
|
28365
|
+
* Telnyx native is ``pcm_16000`` so it stays at 32 bytes/ms).
|
|
28366
|
+
*/
|
|
28367
|
+
trackOutboundPlayback(numBytes) {
|
|
28368
|
+
if (numBytes <= 0) return;
|
|
28369
|
+
const bytesPerMs = this.ttsOutputFormatNativeForCarrier && this.deps.bridge.telephonyProvider !== "telnyx" ? 8 : 32;
|
|
28370
|
+
const now = Date.now();
|
|
28371
|
+
const chunkMs = numBytes / bytesPerMs;
|
|
28372
|
+
const base = this.playbackBufferedUntil > now ? this.playbackBufferedUntil : now;
|
|
28373
|
+
this.playbackBufferedUntil = base + chunkMs;
|
|
28374
|
+
this.turnPlaybackTotalMs += chunkMs;
|
|
28375
|
+
}
|
|
28376
|
+
/**
|
|
28377
|
+
* Estimate the response prefix the caller actually HEARD this turn.
|
|
28378
|
+
*
|
|
28379
|
+
* The pipeline pushes audio faster than realtime, so at barge-in time
|
|
28380
|
+
* ``heard = totalPushed - carrierBacklog`` ms of audio have actually
|
|
28381
|
+
* played. Mapped at sentence granularity against ``turnSpokenSegments``:
|
|
28382
|
+
* a sentence counts as heard once its playback has STARTED
|
|
28383
|
+
* (``startMs <= heardMs``), so the sentence playing at the moment of
|
|
28384
|
+
* interruption is included.
|
|
28385
|
+
*
|
|
28386
|
+
* Returns ``null`` when no segments were tracked this turn (nothing
|
|
28387
|
+
* synthesized through the tracked path — callers fall back to the legacy
|
|
28388
|
+
* full-text behaviour). Mirrors Python ``_heard_response_prefix``.
|
|
28389
|
+
*/
|
|
28390
|
+
heardResponsePrefix() {
|
|
28391
|
+
if (this.turnSpokenSegments.length === 0) return null;
|
|
28392
|
+
const remainingMs = Math.max(0, this.playbackBufferedUntil - Date.now());
|
|
28393
|
+
const heardMs = Math.max(0, this.turnPlaybackTotalMs - remainingMs);
|
|
28394
|
+
const heard = this.turnSpokenSegments.filter((s) => s.startMs <= heardMs);
|
|
28395
|
+
return {
|
|
28396
|
+
text: heard.map((s) => s.text).join(" "),
|
|
28397
|
+
heardEverything: heard.length === this.turnSpokenSegments.length
|
|
28398
|
+
};
|
|
28399
|
+
}
|
|
28400
|
+
/**
|
|
28401
|
+
* Replace the text of the most recent assistant entry in the conversation
|
|
28402
|
+
* history. No-op when the last entry is not an assistant turn (e.g. the
|
|
28403
|
+
* caller's next turn was already committed).
|
|
28404
|
+
*/
|
|
28405
|
+
rewriteLastAssistantEntry(text) {
|
|
28406
|
+
const entries = this.history.entries;
|
|
28407
|
+
const last = entries[entries.length - 1];
|
|
28408
|
+
if (last && last.role === "assistant") {
|
|
28409
|
+
entries[entries.length - 1] = { ...last, text };
|
|
28410
|
+
}
|
|
28411
|
+
}
|
|
28412
|
+
/**
|
|
28413
|
+
* LiveKit-style "heard prefix" semantics for a barge-in that lands AFTER
|
|
28414
|
+
* the turn completed, while the carrier is still playing the buffered
|
|
28415
|
+
* tail.
|
|
28416
|
+
*
|
|
28417
|
+
* The completed turn already recorded its FULL reply in history, but the
|
|
28418
|
+
* caller only heard part of it before interrupting — a stateful agent
|
|
28419
|
+
* runtime (Hermes / OpenClaw) would otherwise "remember saying" things
|
|
28420
|
+
* the caller never heard. Rewrites the last assistant entry to the heard
|
|
28421
|
+
* prefix + ``[interrupted by caller]``.
|
|
28422
|
+
*
|
|
28423
|
+
* MUST run BEFORE ``cancelSpeaking`` resets ``playbackBufferedUntil``
|
|
28424
|
+
* (the backlog is the heard-prefix input). No-op when a turn is still in
|
|
28425
|
+
* flight (the streaming path applies its own marker), when there is no
|
|
28426
|
+
* backlog, or when everything was already heard. Mirrors Python
|
|
28427
|
+
* ``_maybe_truncate_completed_turn_history``.
|
|
28428
|
+
*/
|
|
28429
|
+
maybeTruncateCompletedTurnHistory() {
|
|
28430
|
+
if (this.dispatchTask !== null) return;
|
|
28431
|
+
const remainingMs = this.playbackBufferedUntil - Date.now();
|
|
28432
|
+
if (remainingMs <= 0) return;
|
|
28433
|
+
const heard = this.heardResponsePrefix();
|
|
28434
|
+
if (heard === null || heard.heardEverything) return;
|
|
28435
|
+
this.rewriteLastAssistantEntry(
|
|
28436
|
+
heard.text ? `${heard.text} [interrupted by caller]` : "[interrupted by caller]"
|
|
28437
|
+
);
|
|
28438
|
+
}
|
|
28152
28439
|
/**
|
|
28153
28440
|
* Atomically end speaking AND invalidate any pending grace timer.
|
|
28154
28441
|
* Use instead of ``this.isSpeaking = false`` at barge-in sites.
|
|
@@ -28159,10 +28446,12 @@ Avoid:
|
|
|
28159
28446
|
cancelSpeaking() {
|
|
28160
28447
|
this.speakingGeneration++;
|
|
28161
28448
|
this.isSpeaking = false;
|
|
28449
|
+
this.tailGraceActive = false;
|
|
28162
28450
|
this.speakingStartedAt = null;
|
|
28163
28451
|
this.firstAudioSentAt = null;
|
|
28164
28452
|
this.lastCancelAt = Date.now();
|
|
28165
28453
|
this.suppressedSpeechPending = false;
|
|
28454
|
+
this.playbackBufferedUntil = 0;
|
|
28166
28455
|
this.drainPendingMarks();
|
|
28167
28456
|
if (this.llmAbort !== null) {
|
|
28168
28457
|
try {
|
|
@@ -28235,23 +28524,37 @@ Avoid:
|
|
|
28235
28524
|
if (grace > 0) {
|
|
28236
28525
|
const gen = this.speakingGeneration;
|
|
28237
28526
|
this.clearGraceTimer();
|
|
28238
|
-
|
|
28239
|
-
this.
|
|
28240
|
-
|
|
28241
|
-
this.
|
|
28242
|
-
this.
|
|
28243
|
-
|
|
28244
|
-
|
|
28245
|
-
|
|
28246
|
-
|
|
28247
|
-
this.
|
|
28248
|
-
this.
|
|
28527
|
+
const startTailGrace = () => {
|
|
28528
|
+
this.tailGraceActive = true;
|
|
28529
|
+
this.graceTimer = setTimeout(() => {
|
|
28530
|
+
this.graceTimer = null;
|
|
28531
|
+
if (this.speakingGeneration === gen) {
|
|
28532
|
+
this.isSpeaking = false;
|
|
28533
|
+
this.tailGraceActive = false;
|
|
28534
|
+
this.speakingStartedAt = null;
|
|
28535
|
+
this.firstAudioSentAt = null;
|
|
28536
|
+
this.clearPendingBargeIn();
|
|
28537
|
+
void this.resetBargeInStrategies();
|
|
28538
|
+
if (this.suppressedSpeechPending) {
|
|
28539
|
+
this.suppressedSpeechPending = false;
|
|
28540
|
+
this.flushInboundAudioRing();
|
|
28541
|
+
}
|
|
28542
|
+
this.resetVad();
|
|
28249
28543
|
}
|
|
28250
|
-
|
|
28251
|
-
|
|
28252
|
-
|
|
28544
|
+
}, grace);
|
|
28545
|
+
};
|
|
28546
|
+
const bufferedMs = Math.max(0, this.playbackBufferedUntil - Date.now());
|
|
28547
|
+
if (bufferedMs <= 0) {
|
|
28548
|
+
startTailGrace();
|
|
28549
|
+
} else {
|
|
28550
|
+
this.graceTimer = setTimeout(() => {
|
|
28551
|
+
this.graceTimer = null;
|
|
28552
|
+
if (this.speakingGeneration === gen) startTailGrace();
|
|
28553
|
+
}, bufferedMs);
|
|
28554
|
+
}
|
|
28253
28555
|
} else {
|
|
28254
28556
|
this.isSpeaking = false;
|
|
28557
|
+
this.tailGraceActive = false;
|
|
28255
28558
|
this.speakingStartedAt = null;
|
|
28256
28559
|
this.firstAudioSentAt = null;
|
|
28257
28560
|
this.clearPendingBargeIn();
|
|
@@ -28263,6 +28566,35 @@ Avoid:
|
|
|
28263
28566
|
this.resetVad();
|
|
28264
28567
|
}
|
|
28265
28568
|
}
|
|
28569
|
+
/**
|
|
28570
|
+
* End the post-TTS tail-grace window because the user has begun their next
|
|
28571
|
+
* turn. Unlike a barge-in, the agent's response already played out in full
|
|
28572
|
+
* — there is nothing to cancel and no turn was interrupted. We flip the
|
|
28573
|
+
* speaking flag off (bumping ``speakingGeneration`` so the scheduled grace
|
|
28574
|
+
* timer no-ops), recover any leading audio the self-hearing guard captured
|
|
28575
|
+
* into the ring (the user's first ~250 ms, which VAD needed before it could
|
|
28576
|
+
* emit ``speech_start``), and let the live STT stream take over. We do NOT
|
|
28577
|
+
* call ``sendClear``, ``recordBargeinDetected`` or ``recordTurnInterrupted``
|
|
28578
|
+
* — none apply to a turn that completed normally.
|
|
28579
|
+
*
|
|
28580
|
+
* Without this, fast next-turn speech (humans reply in 200-700 ms, well
|
|
28581
|
+
* inside the 1500 ms default grace) is withheld from STT and recorded as an
|
|
28582
|
+
* empty ``[interrupted]`` turn, after which the agent goes silent for the
|
|
28583
|
+
* rest of the call. Parity with Python ``_end_tail_grace_for_new_turn``.
|
|
28584
|
+
*/
|
|
28585
|
+
endTailGraceForNewTurn() {
|
|
28586
|
+
this.isSpeaking = false;
|
|
28587
|
+
this.tailGraceActive = false;
|
|
28588
|
+
this.speakingStartedAt = null;
|
|
28589
|
+
this.firstAudioSentAt = null;
|
|
28590
|
+
this.playbackBufferedUntil = 0;
|
|
28591
|
+
this.speakingGeneration++;
|
|
28592
|
+
this.clearGraceTimer();
|
|
28593
|
+
this.clearPendingBargeIn();
|
|
28594
|
+
void this.resetBargeInStrategies();
|
|
28595
|
+
this.suppressedSpeechPending = false;
|
|
28596
|
+
this.flushInboundAudioRing();
|
|
28597
|
+
}
|
|
28266
28598
|
async resetBargeInStrategies() {
|
|
28267
28599
|
if (this.bargeInStrategies.length === 0) return;
|
|
28268
28600
|
const { resetStrategies: resetStrategies2 } = await Promise.resolve().then(() => (init_barge_in_strategies(), barge_in_strategies_exports));
|
|
@@ -28398,9 +28730,43 @@ Avoid:
|
|
|
28398
28730
|
maxDurationTimer = null;
|
|
28399
28731
|
transcriptProcessing = false;
|
|
28400
28732
|
transcriptQueue = [];
|
|
28733
|
+
/**
|
|
28734
|
+
* The in-flight turn dispatch (LLM + TTS) runs as a SINGLE tracked promise
|
|
28735
|
+
* so the transcript drain loop keeps running ``handleBargeIn`` against the
|
|
28736
|
+
* LIVE turn during a long (30-90 s) agent-runtime response, instead of
|
|
28737
|
+
* head-of-line-blocking on it. Exactly one is in flight: the launcher awaits
|
|
28738
|
+
* the previous one to settle (fast — a barge-in already aborted it) before
|
|
28739
|
+
* starting the next, preserving history/metrics ordering. Parity with
|
|
28740
|
+
* Python ``_dispatch_task``.
|
|
28741
|
+
*/
|
|
28742
|
+
dispatchTask = null;
|
|
28743
|
+
/**
|
|
28744
|
+
* Cap (ms) on how long teardown waits for the backgrounded dispatch to
|
|
28745
|
+
* settle. JS promises are not cancellable, so a user-supplied ``onMessage``
|
|
28746
|
+
* (which receives no AbortSignal) parked on a hung external call could block
|
|
28747
|
+
* call cleanup indefinitely — `llmAbort.abort()` only unblocks the built-in
|
|
28748
|
+
* LLM/TTS paths. We bound the WAIT (Python hard-cancels the task instead).
|
|
28749
|
+
* 30 s matches the webhook ceiling.
|
|
28750
|
+
*/
|
|
28751
|
+
static DISPATCH_SETTLE_TIMEOUT_MS = 3e4;
|
|
28752
|
+
/**
|
|
28753
|
+
* Opt-in (default OFF): forward inbound audio to STT even while the agent is
|
|
28754
|
+
* speaking, so the transcript barge-in path can receive a transcript on
|
|
28755
|
+
* echo-masked PSTN links where the VAD never fires. ECHO RISK without AEC.
|
|
28756
|
+
* Parity with Python ``_forward_stt_while_speaking``.
|
|
28757
|
+
*/
|
|
28758
|
+
forwardSttWhileSpeaking = ["1", "true", "yes"].includes(
|
|
28759
|
+
(process.env.PATTER_FORWARD_STT_WHILE_SPEAKING ?? "").trim().toLowerCase()
|
|
28760
|
+
);
|
|
28401
28761
|
// Throttle state for back-to-back STT finals — see ``commitTranscript``.
|
|
28402
28762
|
lastCommitText = "";
|
|
28403
28763
|
lastCommitAt = 0;
|
|
28764
|
+
/** The agent's spoken text for the CURRENT turn, accumulated as tokens stream.
|
|
28765
|
+
* The echo guard rejects transcripts matching it (the agent's own TTS bleeding
|
|
28766
|
+
* back into STT when audio is forwarded during TTS without effective AEC).
|
|
28767
|
+
* Reset in ``beginSpeaking``; only consulted while ``forwardSttWhileSpeaking``.
|
|
28768
|
+
* Parity with Python ``_current_agent_spoken_text``. */
|
|
28769
|
+
currentAgentSpokenText = "";
|
|
28404
28770
|
// PCM16 byte-alignment carry for TTS streaming (pipeline mode).
|
|
28405
28771
|
// HTTP streams from ElevenLabs / OpenAI / Cartesia can yield chunks of any
|
|
28406
28772
|
// size, including odd byte counts. Silently dropping the trailing odd byte
|
|
@@ -28420,6 +28786,11 @@ Avoid:
|
|
|
28420
28786
|
this.ws = ws;
|
|
28421
28787
|
this.caller = caller;
|
|
28422
28788
|
this.callee = callee;
|
|
28789
|
+
if (this.forwardSttWhileSpeaking) {
|
|
28790
|
+
getLogger().warn(
|
|
28791
|
+
"PATTER_FORWARD_STT_WHILE_SPEAKING=on: inbound audio is sent to STT during TTS so transcript barge-in works on echo-masked links. Without AEC the agent's own voice may be transcribed as a phantom interruption \u2014 pair with agent.bargeInStrategies."
|
|
28792
|
+
);
|
|
28793
|
+
}
|
|
28423
28794
|
this.bargeInStrategies = (deps.agent.bargeInStrategies ?? []).slice();
|
|
28424
28795
|
const confirmMs = deps.agent.bargeInConfirmMs;
|
|
28425
28796
|
this.bargeInConfirmMs = typeof confirmMs === "number" && Number.isFinite(confirmMs) && confirmMs > 0 ? confirmMs : 1500;
|
|
@@ -28619,12 +28990,12 @@ Avoid:
|
|
|
28619
28990
|
} catch {
|
|
28620
28991
|
}
|
|
28621
28992
|
if (this.deps.onCallStart) {
|
|
28622
|
-
const
|
|
28993
|
+
const direction2 = this.deps.metricsStore.getActive(callId)?.direction ?? "inbound";
|
|
28623
28994
|
await this.deps.onCallStart({
|
|
28624
28995
|
call_id: callId,
|
|
28625
28996
|
caller: this.caller,
|
|
28626
28997
|
callee: this.callee,
|
|
28627
|
-
direction,
|
|
28998
|
+
direction: direction2,
|
|
28628
28999
|
telephony_provider: this.deps.bridge.telephonyProvider,
|
|
28629
29000
|
...Object.keys(customParams).length > 0 ? { custom_params: customParams } : {}
|
|
28630
29001
|
});
|
|
@@ -28691,6 +29062,17 @@ Avoid:
|
|
|
28691
29062
|
setStreamSid(sid) {
|
|
28692
29063
|
this.streamSid = sid;
|
|
28693
29064
|
}
|
|
29065
|
+
/**
|
|
29066
|
+
* Record a terminal/processing error as a coarse, anonymous code on the call
|
|
29067
|
+
* metrics (code only, never the message). Surfaced via `call_completed`
|
|
29068
|
+
* telemetry. Safe to call with any value; last write wins.
|
|
29069
|
+
*/
|
|
29070
|
+
recordError(err) {
|
|
29071
|
+
try {
|
|
29072
|
+
this.metricsAcc.recordError(err);
|
|
29073
|
+
} catch {
|
|
29074
|
+
}
|
|
29075
|
+
}
|
|
28694
29076
|
/** Handle an incoming audio chunk (already decoded from base64). */
|
|
28695
29077
|
/** Forward inbound audio bytes to the AI adapter and (in pipeline mode) the STT provider. */
|
|
28696
29078
|
async handleAudio(audioBuffer) {
|
|
@@ -28717,6 +29099,9 @@ Avoid:
|
|
|
28717
29099
|
);
|
|
28718
29100
|
}
|
|
28719
29101
|
if (evt?.type === "speech_start") {
|
|
29102
|
+
if (this.isSpeaking && this.tailGraceActive) {
|
|
29103
|
+
this.endTailGraceForNewTurn();
|
|
29104
|
+
}
|
|
28720
29105
|
const phantomSuppressed = this.isSpeaking && !this.canBargeIn();
|
|
28721
29106
|
if (phantomSuppressed) {
|
|
28722
29107
|
getLogger().info(
|
|
@@ -28724,7 +29109,8 @@ Avoid:
|
|
|
28724
29109
|
);
|
|
28725
29110
|
this.suppressedSpeechPending = true;
|
|
28726
29111
|
} else if (this.isSpeaking) {
|
|
28727
|
-
|
|
29112
|
+
const deferCancel = this.bargeInStrategies.length > 0 || this.forwardSttWhileSpeaking && !this.aec;
|
|
29113
|
+
if (deferCancel) {
|
|
28728
29114
|
this.startPendingBargeIn();
|
|
28729
29115
|
this.metricsAcc.anchorUserSpeechStart();
|
|
28730
29116
|
return;
|
|
@@ -28734,6 +29120,7 @@ Avoid:
|
|
|
28734
29120
|
this.metricsAcc.recordBargeinDetected();
|
|
28735
29121
|
const bargeinSpan = startSpan(SPAN_BARGEIN, { "patter.call.id": this.callId });
|
|
28736
29122
|
try {
|
|
29123
|
+
this.maybeTruncateCompletedTurnHistory();
|
|
28737
29124
|
this.cancelSpeaking();
|
|
28738
29125
|
try {
|
|
28739
29126
|
this.deps.bridge.sendClear(this.ws, this.streamSid);
|
|
@@ -28778,9 +29165,10 @@ Avoid:
|
|
|
28778
29165
|
if (this.inboundAudioRing.length > _StreamHandler.INBOUND_AUDIO_RING_FRAMES) {
|
|
28779
29166
|
this.inboundAudioRing.shift();
|
|
28780
29167
|
}
|
|
29168
|
+
if (!this.forwardSttWhileSpeaking) return;
|
|
29169
|
+
} else if ((this.deps.agent.bargeInThresholdMs ?? 300) === 0) {
|
|
28781
29170
|
return;
|
|
28782
29171
|
}
|
|
28783
|
-
if ((this.deps.agent.bargeInThresholdMs ?? 300) === 0) return;
|
|
28784
29172
|
}
|
|
28785
29173
|
const hooks = this.deps.agent.hooks;
|
|
28786
29174
|
if (hooks?.beforeSendToStt) {
|
|
@@ -28842,6 +29230,27 @@ Avoid:
|
|
|
28842
29230
|
}
|
|
28843
29231
|
}
|
|
28844
29232
|
}
|
|
29233
|
+
/**
|
|
29234
|
+
* Await the backgrounded turn dispatch during teardown, but never block
|
|
29235
|
+
* longer than ``DISPATCH_SETTLE_TIMEOUT_MS``. The earlier ``llmAbort.abort()``
|
|
29236
|
+
* settles the built-in LLM/TTS paths immediately; the cap only bites a
|
|
29237
|
+
* misbehaving user ``onMessage`` parked on a hung external call (JS promises
|
|
29238
|
+
* can't be cancelled). No-op when nothing is in flight.
|
|
29239
|
+
*/
|
|
29240
|
+
async settleDispatchForTeardown() {
|
|
29241
|
+
if (!this.dispatchTask) return;
|
|
29242
|
+
const settle = this.dispatchTask.catch(() => {
|
|
29243
|
+
});
|
|
29244
|
+
let timer;
|
|
29245
|
+
const cap = new Promise((resolve2) => {
|
|
29246
|
+
timer = setTimeout(resolve2, _StreamHandler.DISPATCH_SETTLE_TIMEOUT_MS);
|
|
29247
|
+
});
|
|
29248
|
+
try {
|
|
29249
|
+
await Promise.race([settle, cap]);
|
|
29250
|
+
} finally {
|
|
29251
|
+
if (timer) clearTimeout(timer);
|
|
29252
|
+
}
|
|
29253
|
+
}
|
|
28845
29254
|
/** Handle call stop / stream end. */
|
|
28846
29255
|
/** Handle a carrier-emitted `stop` event signalling the call has ended. */
|
|
28847
29256
|
async handleStop() {
|
|
@@ -28858,6 +29267,7 @@ Avoid:
|
|
|
28858
29267
|
} catch {
|
|
28859
29268
|
}
|
|
28860
29269
|
}
|
|
29270
|
+
await this.settleDispatchForTeardown();
|
|
28861
29271
|
this.clearPendingBargeIn();
|
|
28862
29272
|
this.drainPendingMarks();
|
|
28863
29273
|
this.clearGraceTimer();
|
|
@@ -28885,6 +29295,7 @@ Avoid:
|
|
|
28885
29295
|
} catch {
|
|
28886
29296
|
}
|
|
28887
29297
|
}
|
|
29298
|
+
await this.settleDispatchForTeardown();
|
|
28888
29299
|
this.clearPendingBargeIn();
|
|
28889
29300
|
this.drainPendingMarks();
|
|
28890
29301
|
this.clearGraceTimer();
|
|
@@ -29279,7 +29690,7 @@ Avoid:
|
|
|
29279
29690
|
};
|
|
29280
29691
|
}
|
|
29281
29692
|
/** Synthesize a single sentence through TTS with hooks, sending audio to telephony. */
|
|
29282
|
-
async synthesizeSentence(sentence, hookExecutor, hookCtx, ttsFirstByteSent) {
|
|
29693
|
+
async synthesizeSentence(sentence, hookExecutor, hookCtx, ttsFirstByteSent, recordSegment = true) {
|
|
29283
29694
|
if (!this.tts || !this.isSpeaking) return;
|
|
29284
29695
|
let transformed = sentence;
|
|
29285
29696
|
const transforms = this.deps.agent.textTransforms;
|
|
@@ -29305,8 +29716,16 @@ Avoid:
|
|
|
29305
29716
|
if (this.aec) {
|
|
29306
29717
|
this.aec.pushFarEnd(processedAudio);
|
|
29307
29718
|
}
|
|
29719
|
+
if (recordSegment) {
|
|
29720
|
+
this.turnSpokenSegments.push({
|
|
29721
|
+
text: processedText,
|
|
29722
|
+
startMs: this.turnPlaybackTotalMs
|
|
29723
|
+
});
|
|
29724
|
+
recordSegment = false;
|
|
29725
|
+
}
|
|
29308
29726
|
const encoded = this.encodePipelineAudio(processedAudio);
|
|
29309
29727
|
this.deps.bridge.sendAudio(this.ws, encoded, this.streamSid);
|
|
29728
|
+
this.trackOutboundPlayback(processedAudio.length);
|
|
29310
29729
|
this.markFirstAudioSent();
|
|
29311
29730
|
}
|
|
29312
29731
|
} catch (e) {
|
|
@@ -29381,64 +29800,101 @@ Avoid:
|
|
|
29381
29800
|
return;
|
|
29382
29801
|
}
|
|
29383
29802
|
this.history.push({ role: "user", text: filteredTranscript, timestamp: Date.now() });
|
|
29384
|
-
let responseText = "";
|
|
29385
29803
|
this.metricsAcc.recordOnUserTurnCompletedDelay(0);
|
|
29386
29804
|
this.metricsAcc.recordTurnCommitted();
|
|
29387
29805
|
closeEndpointSpan();
|
|
29388
|
-
|
|
29389
|
-
|
|
29390
|
-
|
|
29806
|
+
await this.dispatchTask?.catch(() => {
|
|
29807
|
+
});
|
|
29808
|
+
const historySnapshot = [...this.history.entries];
|
|
29809
|
+
this.dispatchTask = this.dispatchTurn(
|
|
29810
|
+
filteredTranscript,
|
|
29811
|
+
hookExecutor,
|
|
29812
|
+
hookCtx,
|
|
29813
|
+
interrupted,
|
|
29814
|
+
historySnapshot
|
|
29815
|
+
);
|
|
29816
|
+
}
|
|
29817
|
+
/**
|
|
29818
|
+
* Post-commit turn body (LLM dispatch → TTS → turn-complete) run as a
|
|
29819
|
+
* tracked background task so the transcript drain loop is not blocked for
|
|
29820
|
+
* the whole (possibly 30-90 s) agent-runtime turn. A barge-in — transcript
|
|
29821
|
+
* (now reachable mid-turn) or VAD — aborts the in-flight ``llmAbort`` and
|
|
29822
|
+
* flips ``isSpeaking``, which the LLM/TTS loops here observe and break on.
|
|
29823
|
+
* Parity with Python ``_dispatch_turn``.
|
|
29824
|
+
*/
|
|
29825
|
+
async dispatchTurn(filteredTranscript, hookExecutor, hookCtx, interrupted, historySnapshot) {
|
|
29826
|
+
const label = this.deps.bridge.label;
|
|
29827
|
+
let responseText = "";
|
|
29828
|
+
try {
|
|
29829
|
+
if (this.deps.onMessage && typeof this.deps.onMessage === "function") {
|
|
29830
|
+
try {
|
|
29831
|
+
responseText = await this.deps.onMessage({
|
|
29832
|
+
text: filteredTranscript,
|
|
29833
|
+
call_id: this.callId,
|
|
29834
|
+
caller: this.caller,
|
|
29835
|
+
callee: this.callee,
|
|
29836
|
+
history: historySnapshot
|
|
29837
|
+
});
|
|
29838
|
+
} catch (e) {
|
|
29839
|
+
getLogger().error(`onMessage error (${label}):`, e);
|
|
29840
|
+
return;
|
|
29841
|
+
}
|
|
29842
|
+
if (!responseText) {
|
|
29843
|
+
getLogger().warn(
|
|
29844
|
+
`onMessage returned empty/void (${label}) \u2014 no TTS will play. If you intended to observe transcripts, use onTranscript instead; if you meant to answer via the built-in LLM, remove onMessage and pass openaiKey.`
|
|
29845
|
+
);
|
|
29846
|
+
}
|
|
29847
|
+
} else if (this.deps.onMessage && isRemoteUrl(this.deps.onMessage)) {
|
|
29848
|
+
const msgData = {
|
|
29391
29849
|
text: filteredTranscript,
|
|
29392
29850
|
call_id: this.callId,
|
|
29393
29851
|
caller: this.caller,
|
|
29394
29852
|
callee: this.callee,
|
|
29395
|
-
history:
|
|
29396
|
-
}
|
|
29397
|
-
|
|
29398
|
-
|
|
29399
|
-
|
|
29400
|
-
|
|
29401
|
-
|
|
29853
|
+
history: historySnapshot
|
|
29854
|
+
};
|
|
29855
|
+
if (isWebSocketUrl(this.deps.onMessage)) {
|
|
29856
|
+
await this.handleWebSocketResponse(msgData);
|
|
29857
|
+
return;
|
|
29858
|
+
}
|
|
29859
|
+
try {
|
|
29860
|
+
responseText = await this.deps.remoteHandler.callWebhook(this.deps.onMessage, msgData);
|
|
29861
|
+
} catch (e) {
|
|
29862
|
+
getLogger().error(`Webhook remote error (${label}):`, e);
|
|
29863
|
+
return;
|
|
29864
|
+
}
|
|
29865
|
+
} else if (this.llmLoop) {
|
|
29866
|
+
const llmResult = await this.runPipelineLlm(
|
|
29867
|
+
filteredTranscript,
|
|
29868
|
+
hookExecutor,
|
|
29869
|
+
hookCtx,
|
|
29870
|
+
historySnapshot
|
|
29871
|
+
);
|
|
29872
|
+
responseText = llmResult.text;
|
|
29873
|
+
interrupted = interrupted || llmResult.interrupted;
|
|
29874
|
+
} else {
|
|
29402
29875
|
getLogger().warn(
|
|
29403
|
-
`
|
|
29876
|
+
`Pipeline (${label}) has no llm/onMessage handler \u2014 transcript "${sanitizeLogValue(filteredTranscript.slice(0, 60))}" dropped. Check that agent.llm or onMessage is configured.`
|
|
29404
29877
|
);
|
|
29405
|
-
}
|
|
29406
|
-
} else if (this.deps.onMessage && isRemoteUrl(this.deps.onMessage)) {
|
|
29407
|
-
const msgData = {
|
|
29408
|
-
text: filteredTranscript,
|
|
29409
|
-
call_id: this.callId,
|
|
29410
|
-
caller: this.caller,
|
|
29411
|
-
callee: this.callee,
|
|
29412
|
-
history: [...this.history.entries]
|
|
29413
|
-
};
|
|
29414
|
-
if (isWebSocketUrl(this.deps.onMessage)) {
|
|
29415
|
-
await this.handleWebSocketResponse(msgData);
|
|
29416
29878
|
return;
|
|
29417
29879
|
}
|
|
29418
|
-
|
|
29419
|
-
|
|
29420
|
-
|
|
29421
|
-
|
|
29422
|
-
|
|
29880
|
+
if (!responseText) return;
|
|
29881
|
+
if (this.llmLoop) {
|
|
29882
|
+
let spokenText = responseText;
|
|
29883
|
+
if (interrupted) {
|
|
29884
|
+
const heard = this.heardResponsePrefix();
|
|
29885
|
+
spokenText = heard === null ? `${responseText} [interrupted by caller]` : heard.text ? `${heard.text} [interrupted by caller]` : "[interrupted by caller]";
|
|
29886
|
+
}
|
|
29887
|
+
await this.emitAssistantTranscript(spokenText);
|
|
29888
|
+
if (!interrupted) this.metricsAcc.recordTtsComplete(responseText);
|
|
29889
|
+
} else {
|
|
29890
|
+
interrupted = await this.runRegularLlm(responseText, hookExecutor, hookCtx) || interrupted;
|
|
29891
|
+
responseText = this.history.entries[this.history.entries.length - 1]?.text ?? responseText;
|
|
29423
29892
|
}
|
|
29424
|
-
|
|
29425
|
-
|
|
29426
|
-
|
|
29427
|
-
|
|
29428
|
-
|
|
29429
|
-
);
|
|
29430
|
-
return;
|
|
29431
|
-
}
|
|
29432
|
-
if (!responseText) return;
|
|
29433
|
-
if (this.llmLoop) {
|
|
29434
|
-
await this.emitAssistantTranscript(responseText);
|
|
29435
|
-
this.metricsAcc.recordTtsComplete(responseText);
|
|
29436
|
-
} else {
|
|
29437
|
-
interrupted = await this.runRegularLlm(responseText, hookExecutor, hookCtx) || interrupted;
|
|
29438
|
-
responseText = this.history.entries[this.history.entries.length - 1]?.text ?? responseText;
|
|
29439
|
-
}
|
|
29440
|
-
if (!interrupted) {
|
|
29441
|
-
await this.emitTurnMetrics(this.metricsAcc.recordTurnComplete(responseText));
|
|
29893
|
+
if (!interrupted) {
|
|
29894
|
+
await this.emitTurnMetrics(this.metricsAcc.recordTurnComplete(responseText));
|
|
29895
|
+
}
|
|
29896
|
+
} finally {
|
|
29897
|
+
this.dispatchTask = null;
|
|
29442
29898
|
}
|
|
29443
29899
|
}
|
|
29444
29900
|
/**
|
|
@@ -29449,6 +29905,18 @@ Avoid:
|
|
|
29449
29905
|
*/
|
|
29450
29906
|
async handleBargeInAsync(transcript) {
|
|
29451
29907
|
if (!transcript.text || !this.isSpeaking) return false;
|
|
29908
|
+
if (this.tailGraceActive) {
|
|
29909
|
+
this.endTailGraceForNewTurn();
|
|
29910
|
+
return false;
|
|
29911
|
+
}
|
|
29912
|
+
if (this.forwardSttWhileSpeaking && looksLikeEcho(transcript.text, this.currentAgentSpokenText)) {
|
|
29913
|
+
getLogger().info(
|
|
29914
|
+
`Barge-in suppressed: transcript matches agent's own speech (echo) \u2014 ${sanitizeLogValue(
|
|
29915
|
+
transcript.text.slice(0, 40)
|
|
29916
|
+
)}`
|
|
29917
|
+
);
|
|
29918
|
+
return false;
|
|
29919
|
+
}
|
|
29452
29920
|
if (!this.canBargeIn()) {
|
|
29453
29921
|
getLogger().info(
|
|
29454
29922
|
`Barge-in transcript suppressed (agent speaking < gate, aec=${this.aec ? "on" : "off"})`
|
|
@@ -29488,6 +29956,18 @@ Avoid:
|
|
|
29488
29956
|
*/
|
|
29489
29957
|
handleBargeIn(transcript) {
|
|
29490
29958
|
if (!transcript.text || !this.isSpeaking) return false;
|
|
29959
|
+
if (this.tailGraceActive) {
|
|
29960
|
+
this.endTailGraceForNewTurn();
|
|
29961
|
+
return false;
|
|
29962
|
+
}
|
|
29963
|
+
if (this.forwardSttWhileSpeaking && looksLikeEcho(transcript.text, this.currentAgentSpokenText)) {
|
|
29964
|
+
getLogger().info(
|
|
29965
|
+
`Barge-in suppressed: transcript matches agent's own speech (echo) \u2014 ${sanitizeLogValue(
|
|
29966
|
+
transcript.text.slice(0, 40)
|
|
29967
|
+
)}`
|
|
29968
|
+
);
|
|
29969
|
+
return false;
|
|
29970
|
+
}
|
|
29491
29971
|
if (this.bargeInStrategies.length === 0) {
|
|
29492
29972
|
if (!this.canBargeIn()) {
|
|
29493
29973
|
getLogger().info(
|
|
@@ -29519,6 +29999,7 @@ Avoid:
|
|
|
29519
29999
|
this.metricsAcc.recordBargeinDetected();
|
|
29520
30000
|
const bargeinSpan = startSpan(SPAN_BARGEIN, { "patter.call.id": this.callId });
|
|
29521
30001
|
try {
|
|
30002
|
+
this.maybeTruncateCompletedTurnHistory();
|
|
29522
30003
|
this.cancelSpeaking();
|
|
29523
30004
|
try {
|
|
29524
30005
|
this.deps.bridge.sendClear(this.ws, this.streamSid);
|
|
@@ -29582,15 +30063,21 @@ Avoid:
|
|
|
29582
30063
|
getLogger().debug(`Dropped likely STT hallucination: ${sanitizeLogValue(normalised.slice(0, 40))}`);
|
|
29583
30064
|
return false;
|
|
29584
30065
|
}
|
|
30066
|
+
if (this.forwardSttWhileSpeaking && this.isSpeaking && looksLikeEcho(text, this.currentAgentSpokenText)) {
|
|
30067
|
+
getLogger().debug(
|
|
30068
|
+
`Dropped agent-echo transcript (not a user turn): ${sanitizeLogValue(normalised.slice(0, 40))}`
|
|
30069
|
+
);
|
|
30070
|
+
return false;
|
|
30071
|
+
}
|
|
29585
30072
|
if (sinceLastMs < 2e3 && normalised === this.lastCommitText) {
|
|
29586
30073
|
getLogger().debug(
|
|
29587
30074
|
`Dropped duplicate final transcript (${(sinceLastMs / 1e3).toFixed(1)}s since last): ${sanitizeLogValue(normalised.slice(0, 40))}`
|
|
29588
30075
|
);
|
|
29589
30076
|
return false;
|
|
29590
30077
|
}
|
|
29591
|
-
if (sinceLastMs < 500) {
|
|
30078
|
+
if (sinceLastMs < 500 && isNearDuplicate(normalised, this.lastCommitText)) {
|
|
29592
30079
|
getLogger().debug(
|
|
29593
|
-
`Dropped back-to-back final
|
|
30080
|
+
`Dropped back-to-back near-duplicate final (${(sinceLastMs / 1e3).toFixed(2)}s since last): ${sanitizeLogValue(normalised.slice(0, 40))}`
|
|
29594
30081
|
);
|
|
29595
30082
|
return false;
|
|
29596
30083
|
}
|
|
@@ -29598,11 +30085,63 @@ Avoid:
|
|
|
29598
30085
|
this.lastCommitAt = now;
|
|
29599
30086
|
return true;
|
|
29600
30087
|
}
|
|
30088
|
+
/**
|
|
30089
|
+
* Schedule the opt-in long-turn filler and return its async ``clear()``.
|
|
30090
|
+
*
|
|
30091
|
+
* When ``agent.longTurnMessage`` is unset / empty the returned clear is a
|
|
30092
|
+
* no-op (byte-identical to today's behaviour). Otherwise a one-shot timer
|
|
30093
|
+
* fires after ``agent.longTurnMessageAfterS`` seconds and, IFF no audio has
|
|
30094
|
+
* reached the carrier this turn (``!ttsFirstByteSent.value``) AND we still own
|
|
30095
|
+
* the floor (``this.isSpeaking``), synthesizes the filler ONCE via the same
|
|
30096
|
+
* per-sentence TTS primitive every sentence uses.
|
|
30097
|
+
*
|
|
30098
|
+
* The returned ``clear()`` is **async**: it stops the timer AND, if the filler
|
|
30099
|
+
* already started synthesizing (its ``setTimeout`` callback runs in a separate
|
|
30100
|
+
* macro-task, so it can fire just before the first real sentence), AWAITS the
|
|
30101
|
+
* in-flight synthesis so the filler audio can never interleave with the real
|
|
30102
|
+
* sentence that follows. Idempotent; self-synthesis failure degrades to
|
|
30103
|
+
* silence (never crashes the turn). The caller must clear on first real audio,
|
|
30104
|
+
* on the error branch, and in the finally.
|
|
30105
|
+
*/
|
|
30106
|
+
scheduleLongTurnFiller(ttsFirstByteSent, hookExecutor, hookCtx, label) {
|
|
30107
|
+
const message = this.deps.agent.longTurnMessage;
|
|
30108
|
+
if (!message) return async () => {
|
|
30109
|
+
};
|
|
30110
|
+
const afterS = this.deps.agent.longTurnMessageAfterS ?? 4;
|
|
30111
|
+
let cancelled = false;
|
|
30112
|
+
let inFlight = null;
|
|
30113
|
+
const timer = setTimeout(() => {
|
|
30114
|
+
if (cancelled || ttsFirstByteSent.value || !this.isSpeaking) return;
|
|
30115
|
+
inFlight = this.synthesizeSentence(
|
|
30116
|
+
message,
|
|
30117
|
+
hookExecutor,
|
|
30118
|
+
hookCtx,
|
|
30119
|
+
ttsFirstByteSent,
|
|
30120
|
+
false
|
|
30121
|
+
).catch((err) => {
|
|
30122
|
+
getLogger().error(
|
|
30123
|
+
`longTurnMessage filler synthesis failed (${label}):`,
|
|
30124
|
+
err
|
|
30125
|
+
);
|
|
30126
|
+
});
|
|
30127
|
+
}, Math.max(0, afterS * 1e3));
|
|
30128
|
+
return async () => {
|
|
30129
|
+
cancelled = true;
|
|
30130
|
+
clearTimeout(timer);
|
|
30131
|
+
if (inFlight !== null) {
|
|
30132
|
+
const pending = inFlight;
|
|
30133
|
+
inFlight = null;
|
|
30134
|
+
await pending;
|
|
30135
|
+
}
|
|
30136
|
+
};
|
|
30137
|
+
}
|
|
29601
30138
|
/**
|
|
29602
30139
|
* Streaming built-in LLM path with sentence chunking and per-sentence
|
|
29603
|
-
* guardrails/TTS. Returns the concatenated response text
|
|
30140
|
+
* guardrails/TTS. Returns the concatenated (plain) response text plus whether
|
|
30141
|
+
* the turn was cut short by a barge-in — the caller applies the interrupted
|
|
30142
|
+
* marker to history only, keeping metrics on the plain text.
|
|
29604
30143
|
*/
|
|
29605
|
-
async runPipelineLlm(filteredTranscript, hookExecutor, hookCtx) {
|
|
30144
|
+
async runPipelineLlm(filteredTranscript, hookExecutor, hookCtx, historySnapshot) {
|
|
29606
30145
|
const label = this.deps.bridge.label;
|
|
29607
30146
|
const callCtx = { call_id: this.callId, caller: this.caller, callee: this.callee };
|
|
29608
30147
|
const chunker = new SentenceChunker({
|
|
@@ -29615,6 +30154,12 @@ Avoid:
|
|
|
29615
30154
|
this.llmAbort = new AbortController();
|
|
29616
30155
|
const llmSignal = this.llmAbort.signal;
|
|
29617
30156
|
let llmError = false;
|
|
30157
|
+
const clearLongTurnFiller = this.scheduleLongTurnFiller(
|
|
30158
|
+
ttsFirstByteSent,
|
|
30159
|
+
hookExecutor,
|
|
30160
|
+
hookCtx,
|
|
30161
|
+
label
|
|
30162
|
+
);
|
|
29618
30163
|
const llmSpan = startSpan(SPAN_LLM, { "patter.call.id": this.callId });
|
|
29619
30164
|
const guardAndSpeak = async (sentence, isFirst) => {
|
|
29620
30165
|
if (isFirst) this.metricsAcc.recordLlmFirstSentenceComplete();
|
|
@@ -29625,6 +30170,7 @@ Avoid:
|
|
|
29625
30170
|
if (transformed === null) return;
|
|
29626
30171
|
sentenceText = transformed;
|
|
29627
30172
|
}
|
|
30173
|
+
await clearLongTurnFiller();
|
|
29628
30174
|
await this.synthesizeSentence(sentenceText, hookExecutor, hookCtx, ttsFirstByteSent);
|
|
29629
30175
|
};
|
|
29630
30176
|
let firstSentenceEmitted = false;
|
|
@@ -29632,7 +30178,7 @@ Avoid:
|
|
|
29632
30178
|
try {
|
|
29633
30179
|
for await (const token of this.llmLoop.run(
|
|
29634
30180
|
filteredTranscript,
|
|
29635
|
-
|
|
30181
|
+
historySnapshot,
|
|
29636
30182
|
callCtx,
|
|
29637
30183
|
this.metricsAcc,
|
|
29638
30184
|
hookExecutor,
|
|
@@ -29643,6 +30189,7 @@ Avoid:
|
|
|
29643
30189
|
this.metricsAcc.recordLlmFirstToken();
|
|
29644
30190
|
await this.emitLlmFirstToken();
|
|
29645
30191
|
allParts.push(token);
|
|
30192
|
+
this.currentAgentSpokenText = allParts.join("");
|
|
29646
30193
|
for (const sentence of chunker.push(token)) {
|
|
29647
30194
|
if (!this.isSpeaking) break;
|
|
29648
30195
|
await guardAndSpeak(sentence, !firstSentenceEmitted);
|
|
@@ -29652,6 +30199,7 @@ Avoid:
|
|
|
29652
30199
|
}
|
|
29653
30200
|
} catch (e) {
|
|
29654
30201
|
const isAbort = e?.name === "AbortError" || llmSignal.aborted;
|
|
30202
|
+
await clearLongTurnFiller();
|
|
29655
30203
|
if (!isAbort) {
|
|
29656
30204
|
llmError = true;
|
|
29657
30205
|
chunker.reset();
|
|
@@ -29660,7 +30208,7 @@ Avoid:
|
|
|
29660
30208
|
const fallback = this.deps.agent.llmErrorMessage;
|
|
29661
30209
|
if (fallback && !ttsFirstByteSent.value && this.isSpeaking) {
|
|
29662
30210
|
try {
|
|
29663
|
-
await this.synthesizeSentence(fallback, hookExecutor, hookCtx, ttsFirstByteSent);
|
|
30211
|
+
await this.synthesizeSentence(fallback, hookExecutor, hookCtx, ttsFirstByteSent, false);
|
|
29664
30212
|
} catch (err) {
|
|
29665
30213
|
getLogger().error(`llmErrorMessage fallback synthesis failed (${label}):`, err);
|
|
29666
30214
|
}
|
|
@@ -29676,6 +30224,7 @@ Avoid:
|
|
|
29676
30224
|
}
|
|
29677
30225
|
}
|
|
29678
30226
|
} finally {
|
|
30227
|
+
await clearLongTurnFiller();
|
|
29679
30228
|
this.endSpeakingWithGrace();
|
|
29680
30229
|
this.llmAbort = null;
|
|
29681
30230
|
try {
|
|
@@ -29683,7 +30232,7 @@ Avoid:
|
|
|
29683
30232
|
} catch {
|
|
29684
30233
|
}
|
|
29685
30234
|
}
|
|
29686
|
-
return allParts.join("");
|
|
30235
|
+
return { text: allParts.join(""), interrupted: llmSignal.aborted };
|
|
29687
30236
|
}
|
|
29688
30237
|
/**
|
|
29689
30238
|
* Non-streaming path (onMessage function / webhook): apply output guardrails,
|
|
@@ -30770,7 +31319,7 @@ function validateTelnyxSignature(rawBody, signature, timestamp, publicKey, toler
|
|
|
30770
31319
|
if (!Number.isFinite(ts)) return false;
|
|
30771
31320
|
const tsMs = ts < 1e12 ? ts * 1e3 : ts;
|
|
30772
31321
|
const ageMs = Date.now() - tsMs;
|
|
30773
|
-
if (ageMs
|
|
31322
|
+
if (ageMs > toleranceSec * 1e3 || ageMs < -TELNYX_FUTURE_SKEW_MS) return false;
|
|
30774
31323
|
const payload = `${timestamp}|${rawBody}`;
|
|
30775
31324
|
const keyBuffer = Buffer.from(publicKey, "base64");
|
|
30776
31325
|
const keyObject = import_node_crypto4.default.createPublicKey({
|
|
@@ -30816,7 +31365,7 @@ function sanitizeVariables(raw) {
|
|
|
30816
31365
|
for (const key of Object.keys(raw)) {
|
|
30817
31366
|
if (BLOCKED_KEYS.has(key)) continue;
|
|
30818
31367
|
const val = raw[key];
|
|
30819
|
-
safe[key] = typeof val === "string" ? val : String(val ?? "");
|
|
31368
|
+
safe[key] = (typeof val === "string" ? val : String(val ?? "")).replace(/[\x00-\x1f\x7f]/g, "").slice(0, 500);
|
|
30820
31369
|
}
|
|
30821
31370
|
return safe;
|
|
30822
31371
|
}
|
|
@@ -30911,7 +31460,7 @@ async function sleep(ms) {
|
|
|
30911
31460
|
if (ms <= 0) return;
|
|
30912
31461
|
await new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
30913
31462
|
}
|
|
30914
|
-
var import_node_crypto4, import_express, import_http, import_ws5, TRANSFER_CALL_TOOL, END_CALL_TOOL, TwilioBridge, TELNYX_DTMF_ALLOWED, TELNYX_DTMF_DURATION_MS, TelnyxBridge, GRACEFUL_SHUTDOWN_TIMEOUT_MS, EmbeddedServer;
|
|
31463
|
+
var import_node_crypto4, import_express, import_http, import_ws5, TRANSFER_CALL_TOOL, END_CALL_TOOL, TELNYX_FUTURE_SKEW_MS, TwilioBridge, TELNYX_DTMF_ALLOWED, TELNYX_DTMF_DURATION_MS, TelnyxBridge, GRACEFUL_SHUTDOWN_TIMEOUT_MS, EmbeddedServer;
|
|
30915
31464
|
var init_server = __esm({
|
|
30916
31465
|
"src/server.ts"() {
|
|
30917
31466
|
"use strict";
|
|
@@ -30920,6 +31469,7 @@ var init_server = __esm({
|
|
|
30920
31469
|
import_express = __toESM(require("express"));
|
|
30921
31470
|
import_http = require("http");
|
|
30922
31471
|
import_ws5 = require("ws");
|
|
31472
|
+
init_call_metrics();
|
|
30923
31473
|
init_openai_realtime_2();
|
|
30924
31474
|
init_elevenlabs_convai();
|
|
30925
31475
|
init_plivo_adapter();
|
|
@@ -30959,6 +31509,7 @@ var init_server = __esm({
|
|
|
30959
31509
|
}
|
|
30960
31510
|
}
|
|
30961
31511
|
};
|
|
31512
|
+
TELNYX_FUTURE_SKEW_MS = 3e4;
|
|
30962
31513
|
TwilioBridge = class {
|
|
30963
31514
|
constructor(config2) {
|
|
30964
31515
|
this.config = config2;
|
|
@@ -31260,6 +31811,9 @@ var init_server = __esm({
|
|
|
31260
31811
|
twilioTokenWarningLogged = false;
|
|
31261
31812
|
telnyxSigWarningLogged = false;
|
|
31262
31813
|
metricsStore;
|
|
31814
|
+
/** Anonymous telemetry client, set by ``client.ts`` ``serve()``; emits the
|
|
31815
|
+
* per-call ``call_completed`` event from the call-end path. */
|
|
31816
|
+
telemetry;
|
|
31263
31817
|
pricing;
|
|
31264
31818
|
remoteHandler = new RemoteMessageHandler();
|
|
31265
31819
|
/**
|
|
@@ -31363,6 +31917,12 @@ var init_server = __esm({
|
|
|
31363
31917
|
* Mirrors Python's ``_resolve_completion``.
|
|
31364
31918
|
*/
|
|
31365
31919
|
resolveCompletion(callId, args) {
|
|
31920
|
+
if (args.outcome === "no_answer" || args.outcome === "busy" || args.outcome === "failed") {
|
|
31921
|
+
recordCallCompleted(this.telemetry, {
|
|
31922
|
+
outcome: args.outcome,
|
|
31923
|
+
carrier: this.config.telephonyProvider
|
|
31924
|
+
});
|
|
31925
|
+
}
|
|
31366
31926
|
const entry = this.completions.get(callId);
|
|
31367
31927
|
if (!entry || entry.done) return;
|
|
31368
31928
|
const data = args.data;
|
|
@@ -32111,7 +32671,13 @@ var init_server = __esm({
|
|
|
32111
32671
|
return Object.fromEntries(Object.entries(snap).filter(([, v]) => v !== void 0));
|
|
32112
32672
|
};
|
|
32113
32673
|
const store = this.metricsStore;
|
|
32674
|
+
const telemetry = this.telemetry;
|
|
32114
32675
|
const wrappedStart = async (data) => {
|
|
32676
|
+
recordCallStarted(telemetry, {
|
|
32677
|
+
providerMode: agent.provider ?? void 0,
|
|
32678
|
+
telephonyProvider: bridge.telephonyProvider,
|
|
32679
|
+
direction: data.direction
|
|
32680
|
+
});
|
|
32115
32681
|
if (logger2.enabled) {
|
|
32116
32682
|
const callId = typeof data.call_id === "string" ? data.call_id : "";
|
|
32117
32683
|
const dataCaller = typeof data.caller === "string" ? data.caller : "";
|
|
@@ -32142,6 +32708,11 @@ var init_server = __esm({
|
|
|
32142
32708
|
if (userMetrics) await userMetrics(data);
|
|
32143
32709
|
};
|
|
32144
32710
|
const wrappedEnd = async (data) => {
|
|
32711
|
+
recordCallCompleted(this.telemetry, {
|
|
32712
|
+
outcome: "completed",
|
|
32713
|
+
metrics: data.metrics,
|
|
32714
|
+
direction: data.direction
|
|
32715
|
+
});
|
|
32145
32716
|
if (logger2.enabled) {
|
|
32146
32717
|
const callId = typeof data.call_id === "string" ? data.call_id : "";
|
|
32147
32718
|
const metricsObj = data.metrics ?? null;
|
|
@@ -32197,7 +32768,7 @@ var init_server = __esm({
|
|
|
32197
32768
|
await handler.handleCallStart(callSid, customParameters);
|
|
32198
32769
|
} else if (event === "media") {
|
|
32199
32770
|
const payload = data.media?.payload ?? "";
|
|
32200
|
-
handler.handleAudio(Buffer.from(payload, "base64"));
|
|
32771
|
+
await handler.handleAudio(Buffer.from(payload, "base64"));
|
|
32201
32772
|
} else if (event === "mark") {
|
|
32202
32773
|
const markName = String(data.mark?.name ?? "");
|
|
32203
32774
|
if (markName) await handler.onMark(markName);
|
|
@@ -32209,6 +32780,7 @@ var init_server = __esm({
|
|
|
32209
32780
|
}
|
|
32210
32781
|
} catch (err) {
|
|
32211
32782
|
getLogger().error("Stream handler error:", err);
|
|
32783
|
+
handler.recordError(err);
|
|
32212
32784
|
}
|
|
32213
32785
|
});
|
|
32214
32786
|
ws.on("close", async () => {
|
|
@@ -32253,7 +32825,7 @@ var init_server = __esm({
|
|
|
32253
32825
|
if (track !== "inbound") return;
|
|
32254
32826
|
const audioChunk = data.media?.payload ?? "";
|
|
32255
32827
|
if (!audioChunk) return;
|
|
32256
|
-
handler.handleAudio(Buffer.from(audioChunk, "base64"));
|
|
32828
|
+
await handler.handleAudio(Buffer.from(audioChunk, "base64"));
|
|
32257
32829
|
} else if (event === "dtmf") {
|
|
32258
32830
|
const digit = String(data.dtmf?.digit ?? "").trim();
|
|
32259
32831
|
if (digit) {
|
|
@@ -32267,9 +32839,11 @@ var init_server = __esm({
|
|
|
32267
32839
|
}
|
|
32268
32840
|
} catch (err) {
|
|
32269
32841
|
getLogger().error("Stream handler error (Telnyx):", err);
|
|
32842
|
+
handler.recordError(err);
|
|
32270
32843
|
}
|
|
32271
32844
|
});
|
|
32272
32845
|
ws.on("close", async () => {
|
|
32846
|
+
this.activeCallIds.delete(ws);
|
|
32273
32847
|
await handler.handleWsClose();
|
|
32274
32848
|
});
|
|
32275
32849
|
}
|
|
@@ -32298,7 +32872,7 @@ var init_server = __esm({
|
|
|
32298
32872
|
await handler.handleCallStart(callId);
|
|
32299
32873
|
} else if (event === "media") {
|
|
32300
32874
|
const payload = data.media?.payload ?? "";
|
|
32301
|
-
if (payload) handler.handleAudio(Buffer.from(payload, "base64"));
|
|
32875
|
+
if (payload) await handler.handleAudio(Buffer.from(payload, "base64"));
|
|
32302
32876
|
} else if (event === "playedStream") {
|
|
32303
32877
|
const markName = String(data.name ?? "");
|
|
32304
32878
|
if (markName) await handler.onMark(markName);
|
|
@@ -32312,6 +32886,7 @@ var init_server = __esm({
|
|
|
32312
32886
|
}
|
|
32313
32887
|
} catch (err) {
|
|
32314
32888
|
getLogger().error("Stream handler error (Plivo):", err);
|
|
32889
|
+
handler.recordError(err);
|
|
32315
32890
|
}
|
|
32316
32891
|
});
|
|
32317
32892
|
ws.on("close", async () => {
|
|
@@ -34192,6 +34767,7 @@ __export(index_exports, {
|
|
|
34192
34767
|
CerebrasLLM: () => LLM4,
|
|
34193
34768
|
ChatContext: () => ChatContext,
|
|
34194
34769
|
CloudflareTunnel: () => CloudflareTunnel,
|
|
34770
|
+
CustomLLM: () => LLM7,
|
|
34195
34771
|
DEFAULT_MIN_SENTENCE_LEN: () => DEFAULT_MIN_SENTENCE_LEN,
|
|
34196
34772
|
DEFAULT_PRICING: () => DEFAULT_PRICING,
|
|
34197
34773
|
DTMF_EVENTS: () => DTMF_EVENTS,
|
|
@@ -34215,7 +34791,7 @@ __export(index_exports, {
|
|
|
34215
34791
|
GoogleLLM: () => LLM5,
|
|
34216
34792
|
GroqLLM: () => LLM3,
|
|
34217
34793
|
Guardrail: () => Guardrail,
|
|
34218
|
-
HermesLLM: () =>
|
|
34794
|
+
HermesLLM: () => LLM8,
|
|
34219
34795
|
IVRActivity: () => IVRActivity,
|
|
34220
34796
|
InworldTTS: () => TTS7,
|
|
34221
34797
|
KrispFrameDuration: () => KrispFrameDuration,
|
|
@@ -34241,7 +34817,7 @@ __export(index_exports, {
|
|
|
34241
34817
|
OpenAITranscribeSTT: () => STT3,
|
|
34242
34818
|
OpenAITranscriptionModel: () => OpenAITranscriptionModel,
|
|
34243
34819
|
OpenAIVoice: () => OpenAIVoice,
|
|
34244
|
-
OpenClawLLM: () =>
|
|
34820
|
+
OpenClawLLM: () => LLM9,
|
|
34245
34821
|
PRICING_LAST_UPDATED: () => PRICING_LAST_UPDATED,
|
|
34246
34822
|
PRICING_VERSION: () => PRICING_VERSION,
|
|
34247
34823
|
PartialStreamError: () => PartialStreamError,
|
|
@@ -34310,6 +34886,7 @@ __export(index_exports, {
|
|
|
34310
34886
|
createResampler24kTo16k: () => createResampler24kTo16k,
|
|
34311
34887
|
createResampler24kTo8k: () => createResampler24kTo8k,
|
|
34312
34888
|
createResampler8kTo16k: () => createResampler8kTo16k,
|
|
34889
|
+
custom: () => custom2,
|
|
34313
34890
|
deepgram: () => deepgram,
|
|
34314
34891
|
defineTool: () => defineTool,
|
|
34315
34892
|
elevenlabs: () => elevenlabs,
|
|
@@ -34321,6 +34898,8 @@ __export(index_exports, {
|
|
|
34321
34898
|
geminiLive: () => geminiLive,
|
|
34322
34899
|
getLogger: () => getLogger,
|
|
34323
34900
|
guardrail: () => guardrail,
|
|
34901
|
+
hashCaller: () => hashCaller,
|
|
34902
|
+
hermes: () => hermes,
|
|
34324
34903
|
initTracing: () => initTracing,
|
|
34325
34904
|
isRemoteUrl: () => isRemoteUrl,
|
|
34326
34905
|
isTracingEnabled: () => isTracingEnabled,
|
|
@@ -34333,7 +34912,9 @@ __export(index_exports, {
|
|
|
34333
34912
|
mountDashboard: () => mountDashboard,
|
|
34334
34913
|
mulawToPcm16: () => mulawToPcm16,
|
|
34335
34914
|
notifyDashboard: () => notifyDashboard,
|
|
34915
|
+
openaiCompatible: () => openaiCompatible,
|
|
34336
34916
|
openaiTts: () => openaiTts,
|
|
34917
|
+
openclaw: () => openclaw,
|
|
34337
34918
|
openclawConsult: () => openclawConsult,
|
|
34338
34919
|
openclawPostCallNotifier: () => openclawPostCallNotifier,
|
|
34339
34920
|
pcm16ToMulaw: () => pcm16ToMulaw,
|
|
@@ -34364,6 +34945,60 @@ init_cjs_shims();
|
|
|
34364
34945
|
init_errors();
|
|
34365
34946
|
init_server();
|
|
34366
34947
|
|
|
34948
|
+
// src/telephony/twilio.ts
|
|
34949
|
+
init_cjs_shims();
|
|
34950
|
+
var Carrier2 = class {
|
|
34951
|
+
kind = "twilio";
|
|
34952
|
+
accountSid;
|
|
34953
|
+
authToken;
|
|
34954
|
+
constructor(opts = {}) {
|
|
34955
|
+
const sid = opts.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
|
|
34956
|
+
const tok = opts.authToken ?? process.env.TWILIO_AUTH_TOKEN;
|
|
34957
|
+
if (!sid) {
|
|
34958
|
+
throw new Error(
|
|
34959
|
+
"Twilio carrier requires accountSid. Pass { accountSid: 'AC...' } or set TWILIO_ACCOUNT_SID in the environment."
|
|
34960
|
+
);
|
|
34961
|
+
}
|
|
34962
|
+
if (!tok) {
|
|
34963
|
+
throw new Error(
|
|
34964
|
+
"Twilio carrier requires authToken. Pass { authToken: '...' } or set TWILIO_AUTH_TOKEN in the environment."
|
|
34965
|
+
);
|
|
34966
|
+
}
|
|
34967
|
+
this.accountSid = sid;
|
|
34968
|
+
this.authToken = tok;
|
|
34969
|
+
}
|
|
34970
|
+
};
|
|
34971
|
+
|
|
34972
|
+
// src/telephony/telnyx.ts
|
|
34973
|
+
init_cjs_shims();
|
|
34974
|
+
var Carrier3 = class {
|
|
34975
|
+
kind = "telnyx";
|
|
34976
|
+
apiKey;
|
|
34977
|
+
connectionId;
|
|
34978
|
+
publicKey;
|
|
34979
|
+
constructor(opts = {}) {
|
|
34980
|
+
const key = opts.apiKey ?? process.env.TELNYX_API_KEY;
|
|
34981
|
+
const conn = opts.connectionId ?? process.env.TELNYX_CONNECTION_ID;
|
|
34982
|
+
const pub = opts.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
|
|
34983
|
+
if (!key) {
|
|
34984
|
+
throw new Error(
|
|
34985
|
+
"Telnyx carrier requires apiKey. Pass { apiKey: '...' } or set TELNYX_API_KEY in the environment."
|
|
34986
|
+
);
|
|
34987
|
+
}
|
|
34988
|
+
if (!conn) {
|
|
34989
|
+
throw new Error(
|
|
34990
|
+
"Telnyx carrier requires connectionId. Pass { connectionId: '...' } or set TELNYX_CONNECTION_ID in the environment."
|
|
34991
|
+
);
|
|
34992
|
+
}
|
|
34993
|
+
this.apiKey = key;
|
|
34994
|
+
this.connectionId = conn;
|
|
34995
|
+
this.publicKey = pub;
|
|
34996
|
+
}
|
|
34997
|
+
};
|
|
34998
|
+
|
|
34999
|
+
// src/client.ts
|
|
35000
|
+
init_plivo();
|
|
35001
|
+
|
|
34367
35002
|
// src/engines/openai.ts
|
|
34368
35003
|
init_cjs_shims();
|
|
34369
35004
|
init_openai_realtime();
|
|
@@ -34590,6 +35225,570 @@ function validateAllToolSchemas(tools) {
|
|
|
34590
35225
|
// src/client.ts
|
|
34591
35226
|
init_logger();
|
|
34592
35227
|
|
|
35228
|
+
// src/telemetry/index.ts
|
|
35229
|
+
init_cjs_shims();
|
|
35230
|
+
|
|
35231
|
+
// src/telemetry/client.ts
|
|
35232
|
+
init_cjs_shims();
|
|
35233
|
+
init_logger();
|
|
35234
|
+
|
|
35235
|
+
// src/telemetry/consent.ts
|
|
35236
|
+
init_cjs_shims();
|
|
35237
|
+
|
|
35238
|
+
// src/telemetry/env.ts
|
|
35239
|
+
init_cjs_shims();
|
|
35240
|
+
var CI_ENV_VARS = [
|
|
35241
|
+
"CI",
|
|
35242
|
+
"CONTINUOUS_INTEGRATION",
|
|
35243
|
+
"GITHUB_ACTIONS",
|
|
35244
|
+
"GITLAB_CI",
|
|
35245
|
+
"TRAVIS",
|
|
35246
|
+
"CIRCLECI",
|
|
35247
|
+
"APPVEYOR",
|
|
35248
|
+
"TF_BUILD",
|
|
35249
|
+
"TEAMCITY_VERSION",
|
|
35250
|
+
"BUILDKITE",
|
|
35251
|
+
"DRONE",
|
|
35252
|
+
"JENKINS_URL",
|
|
35253
|
+
"HUDSON_URL",
|
|
35254
|
+
"BAMBOO_BUILDKEY",
|
|
35255
|
+
"CODEBUILD_BUILD_ID"
|
|
35256
|
+
];
|
|
35257
|
+
var TEST_ENV_VARS = ["VITEST", "JEST_WORKER_ID"];
|
|
35258
|
+
function isTruthy(value) {
|
|
35259
|
+
if (value === void 0) return false;
|
|
35260
|
+
const v = value.trim().toLowerCase();
|
|
35261
|
+
return v !== "" && v !== "0" && v !== "false" && v !== "no" && v !== "off";
|
|
35262
|
+
}
|
|
35263
|
+
function isCi() {
|
|
35264
|
+
return CI_ENV_VARS.some((name) => isTruthy(process.env[name]));
|
|
35265
|
+
}
|
|
35266
|
+
function isTest() {
|
|
35267
|
+
if (TEST_ENV_VARS.some((name) => process.env[name] !== void 0)) return true;
|
|
35268
|
+
const node = (process.env.NODE_ENV ?? "").trim().toLowerCase();
|
|
35269
|
+
const patter = (process.env.PATTER_ENV ?? "").trim().toLowerCase();
|
|
35270
|
+
return node === "test" || patter === "test";
|
|
35271
|
+
}
|
|
35272
|
+
|
|
35273
|
+
// src/telemetry/install-id.ts
|
|
35274
|
+
init_cjs_shims();
|
|
35275
|
+
var import_node_crypto5 = require("crypto");
|
|
35276
|
+
var fs5 = __toESM(require("fs"));
|
|
35277
|
+
var os2 = __toESM(require("os"));
|
|
35278
|
+
var path5 = __toESM(require("path"));
|
|
35279
|
+
var RUN_ID = (0, import_node_crypto5.randomUUID)().replace(/-/g, "");
|
|
35280
|
+
var HEX32 = /^[0-9a-f]{32}$/;
|
|
35281
|
+
var VERSION_RE = /^[0-9][0-9a-z.+-]{0,31}$/;
|
|
35282
|
+
var cachedInstallId = null;
|
|
35283
|
+
function runId() {
|
|
35284
|
+
return RUN_ID;
|
|
35285
|
+
}
|
|
35286
|
+
function statePath() {
|
|
35287
|
+
const base = process.env.PATTER_TELEMETRY_STATE_DIR || process.env.XDG_STATE_HOME;
|
|
35288
|
+
const root = base && base.length > 0 ? base : path5.join(os2.homedir(), ".getpatter");
|
|
35289
|
+
return path5.join(root, "install-id");
|
|
35290
|
+
}
|
|
35291
|
+
function installId() {
|
|
35292
|
+
if (cachedInstallId !== null) return cachedInstallId;
|
|
35293
|
+
const p = statePath();
|
|
35294
|
+
try {
|
|
35295
|
+
const existing = fs5.readFileSync(p, "utf8").trim();
|
|
35296
|
+
if (HEX32.test(existing)) {
|
|
35297
|
+
cachedInstallId = existing;
|
|
35298
|
+
return cachedInstallId;
|
|
35299
|
+
}
|
|
35300
|
+
} catch {
|
|
35301
|
+
}
|
|
35302
|
+
const newId = (0, import_node_crypto5.randomUUID)().replace(/-/g, "");
|
|
35303
|
+
try {
|
|
35304
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
35305
|
+
fs5.writeFileSync(p, newId, "utf8");
|
|
35306
|
+
cachedInstallId = newId;
|
|
35307
|
+
} catch {
|
|
35308
|
+
cachedInstallId = RUN_ID;
|
|
35309
|
+
}
|
|
35310
|
+
return cachedInstallId;
|
|
35311
|
+
}
|
|
35312
|
+
function versionPath() {
|
|
35313
|
+
return path5.join(path5.dirname(statePath()), "version");
|
|
35314
|
+
}
|
|
35315
|
+
function previousVersion(current) {
|
|
35316
|
+
const p = versionPath();
|
|
35317
|
+
let prev = "";
|
|
35318
|
+
try {
|
|
35319
|
+
prev = fs5.readFileSync(p, "utf8").trim();
|
|
35320
|
+
} catch {
|
|
35321
|
+
prev = "";
|
|
35322
|
+
}
|
|
35323
|
+
try {
|
|
35324
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
35325
|
+
fs5.writeFileSync(p, current, "utf8");
|
|
35326
|
+
} catch {
|
|
35327
|
+
}
|
|
35328
|
+
return VERSION_RE.test(prev) ? prev : "";
|
|
35329
|
+
}
|
|
35330
|
+
function daysSinceInstallBucket() {
|
|
35331
|
+
let mtimeMs;
|
|
35332
|
+
try {
|
|
35333
|
+
mtimeMs = fs5.statSync(statePath()).mtimeMs;
|
|
35334
|
+
} catch {
|
|
35335
|
+
return "0";
|
|
35336
|
+
}
|
|
35337
|
+
const days = Math.max(0, Math.floor((Date.now() - mtimeMs) / 864e5));
|
|
35338
|
+
if (days === 0) return "0";
|
|
35339
|
+
if (days <= 7) return "1_7";
|
|
35340
|
+
if (days <= 30) return "8_30";
|
|
35341
|
+
return "30_plus";
|
|
35342
|
+
}
|
|
35343
|
+
function firstRunPath() {
|
|
35344
|
+
return path5.join(path5.dirname(statePath()), "first-run");
|
|
35345
|
+
}
|
|
35346
|
+
function isFirstRun() {
|
|
35347
|
+
const p = firstRunPath();
|
|
35348
|
+
try {
|
|
35349
|
+
if (fs5.existsSync(p)) return false;
|
|
35350
|
+
} catch {
|
|
35351
|
+
return false;
|
|
35352
|
+
}
|
|
35353
|
+
try {
|
|
35354
|
+
fs5.mkdirSync(path5.dirname(p), { recursive: true });
|
|
35355
|
+
fs5.writeFileSync(p, "1", "utf8");
|
|
35356
|
+
return true;
|
|
35357
|
+
} catch {
|
|
35358
|
+
return false;
|
|
35359
|
+
}
|
|
35360
|
+
}
|
|
35361
|
+
function optOutPath() {
|
|
35362
|
+
return path5.join(path5.dirname(statePath()), "telemetry-disabled");
|
|
35363
|
+
}
|
|
35364
|
+
function isOptedOut() {
|
|
35365
|
+
try {
|
|
35366
|
+
return fs5.existsSync(optOutPath());
|
|
35367
|
+
} catch {
|
|
35368
|
+
return false;
|
|
35369
|
+
}
|
|
35370
|
+
}
|
|
35371
|
+
|
|
35372
|
+
// src/telemetry/consent.ts
|
|
35373
|
+
function isEnabled(flag) {
|
|
35374
|
+
if (isTruthy(process.env.DO_NOT_TRACK)) return false;
|
|
35375
|
+
if (isTruthy(process.env.PATTER_TELEMETRY_DISABLED)) return false;
|
|
35376
|
+
if (isOptedOut()) return false;
|
|
35377
|
+
if (flag === false) return false;
|
|
35378
|
+
if (isCi() || isTest()) return false;
|
|
35379
|
+
return true;
|
|
35380
|
+
}
|
|
35381
|
+
|
|
35382
|
+
// src/telemetry/events.ts
|
|
35383
|
+
init_cjs_shims();
|
|
35384
|
+
var os3 = __toESM(require("os"));
|
|
35385
|
+
|
|
35386
|
+
// src/telemetry/stack.ts
|
|
35387
|
+
init_cjs_shims();
|
|
35388
|
+
var STACK_VENDORS = /* @__PURE__ */ new Set([
|
|
35389
|
+
"openai",
|
|
35390
|
+
"anthropic",
|
|
35391
|
+
"google",
|
|
35392
|
+
"cerebras",
|
|
35393
|
+
"groq",
|
|
35394
|
+
"deepgram",
|
|
35395
|
+
"elevenlabs",
|
|
35396
|
+
"cartesia",
|
|
35397
|
+
"whisper",
|
|
35398
|
+
"soniox",
|
|
35399
|
+
"assemblyai",
|
|
35400
|
+
"speechmatics",
|
|
35401
|
+
"lmnt",
|
|
35402
|
+
"rime",
|
|
35403
|
+
"inworld",
|
|
35404
|
+
"telnyx",
|
|
35405
|
+
"other"
|
|
35406
|
+
]);
|
|
35407
|
+
var VENDOR_ALIASES = {
|
|
35408
|
+
cartesia_stt: "cartesia",
|
|
35409
|
+
cartesia_tts: "cartesia",
|
|
35410
|
+
openai_tts: "openai",
|
|
35411
|
+
openai_transcribe: "openai",
|
|
35412
|
+
elevenlabs_ws: "elevenlabs",
|
|
35413
|
+
telnyx_stt: "telnyx",
|
|
35414
|
+
telnyx_tts: "telnyx"
|
|
35415
|
+
};
|
|
35416
|
+
var RAW_UNSAFE_RE = /[^a-z0-9._-]/;
|
|
35417
|
+
var DATE_SUFFIX_RE = /-\d{8}$/;
|
|
35418
|
+
function vendorOf(providerKey) {
|
|
35419
|
+
if (!providerKey) return "other";
|
|
35420
|
+
const v = VENDOR_ALIASES[providerKey] ?? providerKey;
|
|
35421
|
+
return STACK_VENDORS.has(v) ? v : "other";
|
|
35422
|
+
}
|
|
35423
|
+
function modelToken(vendor, rawModel) {
|
|
35424
|
+
if (!rawModel) return `${vendor}-other`;
|
|
35425
|
+
const m = rawModel.trim().toLowerCase();
|
|
35426
|
+
if (m.length > 40 || RAW_UNSAFE_RE.test(m)) return `${vendor}-other`;
|
|
35427
|
+
const token = m.replace(/_/g, "-").replace(DATE_SUFFIX_RE, "").replace(/^[-.]+|[-.]+$/g, "");
|
|
35428
|
+
return token ? `${vendor}-${token}` : `${vendor}-other`;
|
|
35429
|
+
}
|
|
35430
|
+
function readProviderKey(obj) {
|
|
35431
|
+
const ctor = obj?.constructor;
|
|
35432
|
+
const key = ctor?.providerKey;
|
|
35433
|
+
return typeof key === "string" && key ? key : null;
|
|
35434
|
+
}
|
|
35435
|
+
function readModel(obj) {
|
|
35436
|
+
const rec = obj;
|
|
35437
|
+
for (const attr of ["model", "modelId", "_model"]) {
|
|
35438
|
+
const v = rec?.[attr];
|
|
35439
|
+
if (typeof v === "string" && v) return v;
|
|
35440
|
+
}
|
|
35441
|
+
return "";
|
|
35442
|
+
}
|
|
35443
|
+
function layerDims(obj, providerField, modelField) {
|
|
35444
|
+
if (obj === null || obj === void 0) return {};
|
|
35445
|
+
const vendor = vendorOf(readProviderKey(obj));
|
|
35446
|
+
return { [providerField]: vendor, [modelField]: modelToken(vendor, readModel(obj)) };
|
|
35447
|
+
}
|
|
35448
|
+
function stackDimensions(stt, tts, llm) {
|
|
35449
|
+
return {
|
|
35450
|
+
...layerDims(stt, "stt_provider", "stt_model"),
|
|
35451
|
+
...layerDims(tts, "tts_provider", "tts_model"),
|
|
35452
|
+
...layerDims(llm, "llm_provider", "llm_model")
|
|
35453
|
+
};
|
|
35454
|
+
}
|
|
35455
|
+
|
|
35456
|
+
// src/telemetry/events.ts
|
|
35457
|
+
var SCHEMA_VERSION2 = 5;
|
|
35458
|
+
var EVENT_SDK_INITIALIZED = "sdk_initialized";
|
|
35459
|
+
var EVENT_FIRST_RUN = "first_run";
|
|
35460
|
+
var EVENT_CLI_COMMAND = "cli_command";
|
|
35461
|
+
var EVENT_FEATURE_USED = "feature_used";
|
|
35462
|
+
var EVENT_AGENT_CONFIGURED = "agent_configured";
|
|
35463
|
+
var EVENT_CALL_STARTED = "call_started";
|
|
35464
|
+
var EVENT_CALL_COMPLETED = "call_completed";
|
|
35465
|
+
var ALLOWED_EVENTS = /* @__PURE__ */ new Set([
|
|
35466
|
+
EVENT_SDK_INITIALIZED,
|
|
35467
|
+
EVENT_FIRST_RUN,
|
|
35468
|
+
EVENT_CLI_COMMAND,
|
|
35469
|
+
EVENT_FEATURE_USED,
|
|
35470
|
+
EVENT_AGENT_CONFIGURED,
|
|
35471
|
+
EVENT_CALL_STARTED,
|
|
35472
|
+
EVENT_CALL_COMPLETED
|
|
35473
|
+
]);
|
|
35474
|
+
var DIMENSION_VALUES = {
|
|
35475
|
+
carrier: /* @__PURE__ */ new Set(["twilio", "telnyx", "plivo", "none"]),
|
|
35476
|
+
tunnel: /* @__PURE__ */ new Set(["static", "configured", "none"]),
|
|
35477
|
+
engine: /* @__PURE__ */ new Set(["realtime", "convai", "pipeline"]),
|
|
35478
|
+
provider: /* @__PURE__ */ new Set([
|
|
35479
|
+
"openai",
|
|
35480
|
+
"elevenlabs",
|
|
35481
|
+
"deepgram",
|
|
35482
|
+
"cartesia",
|
|
35483
|
+
"cerebras",
|
|
35484
|
+
"anthropic",
|
|
35485
|
+
"google",
|
|
35486
|
+
"whisper",
|
|
35487
|
+
"other"
|
|
35488
|
+
]),
|
|
35489
|
+
// agent_configured dimensions
|
|
35490
|
+
custom_tool_count_bucket: /* @__PURE__ */ new Set(["0", "1", "2_3", "4_6", "7_12", "13_plus"]),
|
|
35491
|
+
integration: /* @__PURE__ */ new Set(["openclaw", "mcp", "hermes", "other", "none"]),
|
|
35492
|
+
integration_kind: /* @__PURE__ */ new Set(["consult", "mcp", "none"]),
|
|
35493
|
+
mcp_server_count_bucket: /* @__PURE__ */ new Set(["0", "1", "2_3", "4_plus"]),
|
|
35494
|
+
// call_started / call_completed: inbound vs outbound — a core usage split.
|
|
35495
|
+
direction: /* @__PURE__ */ new Set(["inbound", "outbound", "none"]),
|
|
35496
|
+
// cli_command: which CLI subcommand was invoked (never args/flags values).
|
|
35497
|
+
cli_command: /* @__PURE__ */ new Set(["dashboard", "eval", "telemetry", "none", "other"]),
|
|
35498
|
+
// call_completed: the call's terminal outcome
|
|
35499
|
+
outcome: /* @__PURE__ */ new Set(["completed", "error", "no_answer", "busy", "failed"]),
|
|
35500
|
+
// call_completed: terminal error code (mirrors ErrorCode, plus "other"). Never
|
|
35501
|
+
// the error message.
|
|
35502
|
+
error_code: /* @__PURE__ */ new Set([
|
|
35503
|
+
"config",
|
|
35504
|
+
"connection",
|
|
35505
|
+
"auth",
|
|
35506
|
+
"timeout",
|
|
35507
|
+
"rate_limit",
|
|
35508
|
+
"webhook_verification",
|
|
35509
|
+
"input_validation",
|
|
35510
|
+
"provider_error",
|
|
35511
|
+
"provision",
|
|
35512
|
+
"internal",
|
|
35513
|
+
"other"
|
|
35514
|
+
]),
|
|
35515
|
+
// feature_used (pipeline): per-layer vendor of the composed stack. A
|
|
35516
|
+
// providerKey not on the closed allowlist collapses to "other"; an absent layer
|
|
35517
|
+
// is omitted (the value set keeps "none" only as a safety token).
|
|
35518
|
+
stt_provider: /* @__PURE__ */ new Set([...STACK_VENDORS, "none"]),
|
|
35519
|
+
tts_provider: /* @__PURE__ */ new Set([...STACK_VENDORS, "none"]),
|
|
35520
|
+
llm_provider: /* @__PURE__ */ new Set([...STACK_VENDORS, "none"]),
|
|
35521
|
+
// sdk_initialized: anonymous deploy-shape (presence-only env/file probes).
|
|
35522
|
+
invoked_by_agent: /* @__PURE__ */ new Set(["claude", "cursor", "copilot", "gemini", "windsurf", "other", "none"]),
|
|
35523
|
+
serverless: /* @__PURE__ */ new Set(["lambda", "cloud_run", "vercel", "azure_functions", "none"]),
|
|
35524
|
+
cloud: /* @__PURE__ */ new Set(["aws", "gcp", "azure", "fly", "none"]),
|
|
35525
|
+
package_manager: /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun", "pip", "uv", "poetry", "pipenv", "conda", "none"]),
|
|
35526
|
+
days_since_install_bucket: /* @__PURE__ */ new Set(["0", "1_7", "8_30", "30_plus"]),
|
|
35527
|
+
// agent_configured: feature-adoption (Realtime tuning).
|
|
35528
|
+
noise_reduction: /* @__PURE__ */ new Set(["near_field", "far_field", "none"]),
|
|
35529
|
+
turn_detection: /* @__PURE__ */ new Set(["default", "custom", "none"]),
|
|
35530
|
+
// call_completed: how many conversational turns the call had.
|
|
35531
|
+
turn_count_bucket: /* @__PURE__ */ new Set(["0", "1", "2_3", "4_6", "7_12", "13_plus"])
|
|
35532
|
+
};
|
|
35533
|
+
var NUMERIC_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
35534
|
+
"builtin_tool_count",
|
|
35535
|
+
"latency_ms",
|
|
35536
|
+
"duration_seconds",
|
|
35537
|
+
"cost_usd"
|
|
35538
|
+
]);
|
|
35539
|
+
var STRING_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
35540
|
+
"stt_model",
|
|
35541
|
+
"tts_model",
|
|
35542
|
+
"llm_model",
|
|
35543
|
+
"previous_sdk_version"
|
|
35544
|
+
]);
|
|
35545
|
+
var MODEL_TOKEN_RE = /^[a-z0-9][a-z0-9.-]{0,40}$/;
|
|
35546
|
+
var BOOL_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
35547
|
+
"container",
|
|
35548
|
+
"preambles_used",
|
|
35549
|
+
"per_tool_timeouts_set",
|
|
35550
|
+
"llm_fallback_configured"
|
|
35551
|
+
]);
|
|
35552
|
+
var ALLOWED_DIMENSIONS = /* @__PURE__ */ new Set([
|
|
35553
|
+
...Object.keys(DIMENSION_VALUES),
|
|
35554
|
+
...NUMERIC_DIMENSIONS,
|
|
35555
|
+
...STRING_DIMENSIONS,
|
|
35556
|
+
...BOOL_DIMENSIONS
|
|
35557
|
+
]);
|
|
35558
|
+
function osFamily() {
|
|
35559
|
+
const p = os3.platform();
|
|
35560
|
+
if (p === "win32") return "windows";
|
|
35561
|
+
return p || "unknown";
|
|
35562
|
+
}
|
|
35563
|
+
function arch2() {
|
|
35564
|
+
const a = os3.arch();
|
|
35565
|
+
if (a === "x64") return "x86_64";
|
|
35566
|
+
if (a === "arm64") return "arm64";
|
|
35567
|
+
return "other";
|
|
35568
|
+
}
|
|
35569
|
+
function runtimeVersion() {
|
|
35570
|
+
const parts = (process.versions.node ?? "0.0").split(".");
|
|
35571
|
+
return `${parts[0] ?? "0"}.${parts[1] ?? "0"}`;
|
|
35572
|
+
}
|
|
35573
|
+
function buildEvent(name, opts) {
|
|
35574
|
+
if (!ALLOWED_EVENTS.has(name)) {
|
|
35575
|
+
throw new Error(`unknown telemetry event: ${name}`);
|
|
35576
|
+
}
|
|
35577
|
+
const event = {
|
|
35578
|
+
event: name,
|
|
35579
|
+
schema_version: SCHEMA_VERSION2,
|
|
35580
|
+
run_id: runId(),
|
|
35581
|
+
install_id: installId(),
|
|
35582
|
+
sdk: "typescript",
|
|
35583
|
+
sdk_version: opts.sdkVersion,
|
|
35584
|
+
os: osFamily(),
|
|
35585
|
+
arch: arch2(),
|
|
35586
|
+
runtime: "node",
|
|
35587
|
+
runtime_version: runtimeVersion(),
|
|
35588
|
+
ci: isCi() || isTest()
|
|
35589
|
+
};
|
|
35590
|
+
for (const [key, raw] of Object.entries(opts.dimensions ?? {})) {
|
|
35591
|
+
if (!ALLOWED_DIMENSIONS.has(key) || raw === null || raw === void 0) {
|
|
35592
|
+
continue;
|
|
35593
|
+
}
|
|
35594
|
+
let value = raw;
|
|
35595
|
+
const allowed = DIMENSION_VALUES[key];
|
|
35596
|
+
if (allowed && !(typeof value === "string" && allowed.has(value))) {
|
|
35597
|
+
value = "other";
|
|
35598
|
+
} else if (STRING_DIMENSIONS.has(key)) {
|
|
35599
|
+
if (!(typeof value === "string" && MODEL_TOKEN_RE.test(value))) {
|
|
35600
|
+
continue;
|
|
35601
|
+
}
|
|
35602
|
+
} else if (BOOL_DIMENSIONS.has(key) && typeof value !== "boolean") {
|
|
35603
|
+
continue;
|
|
35604
|
+
}
|
|
35605
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
35606
|
+
event[key] = value;
|
|
35607
|
+
}
|
|
35608
|
+
}
|
|
35609
|
+
return event;
|
|
35610
|
+
}
|
|
35611
|
+
|
|
35612
|
+
// src/telemetry/client.ts
|
|
35613
|
+
var DEFAULT_ENDPOINT = "https://telemetry.getpatter.com/v1/ingest";
|
|
35614
|
+
var TIMEOUT_MS = 3e3;
|
|
35615
|
+
var BUFFER_MAX = 256;
|
|
35616
|
+
var noticeShown = false;
|
|
35617
|
+
var liveClients = /* @__PURE__ */ new Set();
|
|
35618
|
+
var exitHookRegistered = false;
|
|
35619
|
+
function showNoticeOnce() {
|
|
35620
|
+
if (noticeShown) return;
|
|
35621
|
+
noticeShown = true;
|
|
35622
|
+
getLogger().info(
|
|
35623
|
+
"Anonymous usage telemetry is on (no PII, no call content). Collected: a random anonymous install id, SDK version, language, OS family, runtime version, coarse feature flags, the composed stack (provider + model per layer), tool counts, integration category, and per-call duration, latency, cost, and error codes (no call content, no message text). Disable with PATTER_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1, or telemetry: false. Details: https://docs.getpatter.com/telemetry"
|
|
35624
|
+
);
|
|
35625
|
+
}
|
|
35626
|
+
function registerExitHook() {
|
|
35627
|
+
if (exitHookRegistered) return;
|
|
35628
|
+
exitHookRegistered = true;
|
|
35629
|
+
process.once("beforeExit", () => {
|
|
35630
|
+
for (const ref of [...liveClients]) {
|
|
35631
|
+
const client = ref.deref();
|
|
35632
|
+
if (client) void client.close();
|
|
35633
|
+
else liveClients.delete(ref);
|
|
35634
|
+
}
|
|
35635
|
+
});
|
|
35636
|
+
}
|
|
35637
|
+
var TelemetryClient = class {
|
|
35638
|
+
sdkVersion;
|
|
35639
|
+
enabledFlag;
|
|
35640
|
+
endpoint;
|
|
35641
|
+
debug;
|
|
35642
|
+
buffer = [];
|
|
35643
|
+
flushing = false;
|
|
35644
|
+
closed = false;
|
|
35645
|
+
selfRef = new WeakRef(this);
|
|
35646
|
+
constructor(options) {
|
|
35647
|
+
this.sdkVersion = options.sdkVersion;
|
|
35648
|
+
this.enabledFlag = isEnabled(options.flag);
|
|
35649
|
+
this.endpoint = options.endpoint ?? process.env.PATTER_TELEMETRY_ENDPOINT ?? DEFAULT_ENDPOINT;
|
|
35650
|
+
this.debug = isTruthy(process.env.PATTER_TELEMETRY_DEBUG);
|
|
35651
|
+
if (this.enabledFlag && !this.debug) {
|
|
35652
|
+
showNoticeOnce();
|
|
35653
|
+
registerExitHook();
|
|
35654
|
+
liveClients.add(this.selfRef);
|
|
35655
|
+
}
|
|
35656
|
+
}
|
|
35657
|
+
get enabled() {
|
|
35658
|
+
return this.enabledFlag;
|
|
35659
|
+
}
|
|
35660
|
+
/** Enqueue an event. Fire-and-forget; never throws, never blocks. */
|
|
35661
|
+
record(name, dimensions) {
|
|
35662
|
+
if (!this.enabledFlag || this.closed) return;
|
|
35663
|
+
let event;
|
|
35664
|
+
try {
|
|
35665
|
+
event = buildEvent(name, { sdkVersion: this.sdkVersion, dimensions });
|
|
35666
|
+
} catch (err) {
|
|
35667
|
+
getLogger().debug("telemetry buildEvent failed", err);
|
|
35668
|
+
return;
|
|
35669
|
+
}
|
|
35670
|
+
if (this.debug) {
|
|
35671
|
+
try {
|
|
35672
|
+
process.stderr.write(`[patter telemetry] ${JSON.stringify(event)}
|
|
35673
|
+
`);
|
|
35674
|
+
} catch {
|
|
35675
|
+
}
|
|
35676
|
+
return;
|
|
35677
|
+
}
|
|
35678
|
+
try {
|
|
35679
|
+
if (this.buffer.length >= BUFFER_MAX) this.buffer.shift();
|
|
35680
|
+
this.buffer.push(event);
|
|
35681
|
+
this.scheduleFlush();
|
|
35682
|
+
} catch (err) {
|
|
35683
|
+
getLogger().debug("telemetry enqueue failed", err);
|
|
35684
|
+
}
|
|
35685
|
+
}
|
|
35686
|
+
/**
|
|
35687
|
+
* Schedule a flush of any buffered events. Events recorded before the server
|
|
35688
|
+
* is running (e.g. at `new Patter(...)`) sit in the buffer; call this once the
|
|
35689
|
+
* server is up so they ship promptly. Cheap when disabled or buffer is empty.
|
|
35690
|
+
*/
|
|
35691
|
+
flushPending() {
|
|
35692
|
+
if (!this.enabledFlag || this.debug) return;
|
|
35693
|
+
try {
|
|
35694
|
+
this.scheduleFlush();
|
|
35695
|
+
} catch (err) {
|
|
35696
|
+
getLogger().debug("telemetry flushPending failed", err);
|
|
35697
|
+
}
|
|
35698
|
+
}
|
|
35699
|
+
/** Flush remaining events (graceful shutdown). Never throws. */
|
|
35700
|
+
async close() {
|
|
35701
|
+
if (this.closed) return;
|
|
35702
|
+
this.closed = true;
|
|
35703
|
+
liveClients.delete(this.selfRef);
|
|
35704
|
+
if (!this.enabledFlag || this.debug) return;
|
|
35705
|
+
try {
|
|
35706
|
+
await this.flush();
|
|
35707
|
+
} catch (err) {
|
|
35708
|
+
getLogger().debug("telemetry close flush failed", err);
|
|
35709
|
+
}
|
|
35710
|
+
}
|
|
35711
|
+
scheduleFlush() {
|
|
35712
|
+
if (this.flushing) return;
|
|
35713
|
+
this.flushing = true;
|
|
35714
|
+
void this.flush().finally(() => {
|
|
35715
|
+
this.flushing = false;
|
|
35716
|
+
});
|
|
35717
|
+
}
|
|
35718
|
+
async flush() {
|
|
35719
|
+
if (this.buffer.length === 0) return;
|
|
35720
|
+
const events = this.buffer.splice(0, this.buffer.length);
|
|
35721
|
+
const controller = new AbortController();
|
|
35722
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
35723
|
+
timer.unref?.();
|
|
35724
|
+
try {
|
|
35725
|
+
await fetch(this.endpoint, {
|
|
35726
|
+
method: "POST",
|
|
35727
|
+
headers: { "content-type": "application/json" },
|
|
35728
|
+
body: JSON.stringify(events),
|
|
35729
|
+
signal: controller.signal
|
|
35730
|
+
});
|
|
35731
|
+
} catch (err) {
|
|
35732
|
+
getLogger().debug("telemetry flush failed", err);
|
|
35733
|
+
} finally {
|
|
35734
|
+
clearTimeout(timer);
|
|
35735
|
+
}
|
|
35736
|
+
}
|
|
35737
|
+
};
|
|
35738
|
+
|
|
35739
|
+
// src/telemetry/environment.ts
|
|
35740
|
+
init_cjs_shims();
|
|
35741
|
+
var fs6 = __toESM(require("fs"));
|
|
35742
|
+
var env = process.env;
|
|
35743
|
+
function invokedByAgent() {
|
|
35744
|
+
if ("CLAUDECODE" in env || "CLAUDE_CODE" in env || "CLAUDE_CODE_ENTRYPOINT" in env)
|
|
35745
|
+
return "claude";
|
|
35746
|
+
if ("CURSOR_TRACE_ID" in env || "CURSOR_AGENT" in env) return "cursor";
|
|
35747
|
+
if ("GITHUB_COPILOT_AGENT" in env || "COPILOT_AGENT_ID" in env) return "copilot";
|
|
35748
|
+
if ("GEMINI_CLI" in env || "GEMINI_AGENT" in env) return "gemini";
|
|
35749
|
+
if ("WINDSURF" in env || "WINDSURF_AGENT" in env) return "windsurf";
|
|
35750
|
+
if ("AIDER" in env || "OPENAI_AGENT" in env) return "other";
|
|
35751
|
+
return "none";
|
|
35752
|
+
}
|
|
35753
|
+
function inContainer() {
|
|
35754
|
+
try {
|
|
35755
|
+
if (fs6.existsSync("/.dockerenv")) return true;
|
|
35756
|
+
} catch {
|
|
35757
|
+
}
|
|
35758
|
+
if (env.KUBERNETES_SERVICE_HOST) return true;
|
|
35759
|
+
try {
|
|
35760
|
+
const blob = fs6.readFileSync("/proc/1/cgroup", "utf8");
|
|
35761
|
+
return blob.includes("docker") || blob.includes("containerd") || blob.includes("kubepods");
|
|
35762
|
+
} catch {
|
|
35763
|
+
return false;
|
|
35764
|
+
}
|
|
35765
|
+
}
|
|
35766
|
+
function serverless() {
|
|
35767
|
+
if (env.AWS_LAMBDA_FUNCTION_NAME) return "lambda";
|
|
35768
|
+
if (env.K_SERVICE) return "cloud_run";
|
|
35769
|
+
if (env.VERCEL) return "vercel";
|
|
35770
|
+
if (env.AZURE_FUNCTIONS_ENVIRONMENT || env.FUNCTIONS_WORKER_RUNTIME) return "azure_functions";
|
|
35771
|
+
return "none";
|
|
35772
|
+
}
|
|
35773
|
+
function cloud() {
|
|
35774
|
+
if (env.AWS_REGION || env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return "aws";
|
|
35775
|
+
if (env.K_SERVICE || env.GOOGLE_CLOUD_PROJECT || env.GCP_PROJECT) return "gcp";
|
|
35776
|
+
if (env.WEBSITE_INSTANCE_ID || env.AZURE_FUNCTIONS_ENVIRONMENT) return "azure";
|
|
35777
|
+
if (env.FLY_APP_NAME) return "fly";
|
|
35778
|
+
return "none";
|
|
35779
|
+
}
|
|
35780
|
+
function packageManager() {
|
|
35781
|
+
const ua = env.npm_config_user_agent ?? "";
|
|
35782
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
35783
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
35784
|
+
if (ua.startsWith("bun")) return "bun";
|
|
35785
|
+
if (ua.startsWith("npm")) return "npm";
|
|
35786
|
+
return "none";
|
|
35787
|
+
}
|
|
35788
|
+
|
|
35789
|
+
// src/client.ts
|
|
35790
|
+
init_version();
|
|
35791
|
+
|
|
34593
35792
|
// src/_speech-events.ts
|
|
34594
35793
|
init_cjs_shims();
|
|
34595
35794
|
init_logger();
|
|
@@ -34895,6 +36094,79 @@ function closeParkedConnections(slot) {
|
|
|
34895
36094
|
}
|
|
34896
36095
|
}
|
|
34897
36096
|
}
|
|
36097
|
+
function carrierFamily2(carrier) {
|
|
36098
|
+
if (carrier instanceof Carrier2) return "twilio";
|
|
36099
|
+
if (carrier instanceof Carrier3) return "telnyx";
|
|
36100
|
+
if (carrier instanceof Carrier) return "plivo";
|
|
36101
|
+
return "none";
|
|
36102
|
+
}
|
|
36103
|
+
function telemetryEngineFamily(opts) {
|
|
36104
|
+
if (opts.engine) {
|
|
36105
|
+
return opts.engine.constructor.name.toLowerCase().includes("convai") ? "convai" : "realtime";
|
|
36106
|
+
}
|
|
36107
|
+
if (opts.provider === "elevenlabs_convai") return "convai";
|
|
36108
|
+
if (opts.provider === "pipeline") return "pipeline";
|
|
36109
|
+
if (opts.provider === "openai_realtime") return "realtime";
|
|
36110
|
+
if (opts.stt || opts.tts) return "pipeline";
|
|
36111
|
+
return "realtime";
|
|
36112
|
+
}
|
|
36113
|
+
function telemetryProviderFamily(family) {
|
|
36114
|
+
if (family === "realtime") return "openai";
|
|
36115
|
+
if (family === "convai") return "elevenlabs";
|
|
36116
|
+
return "other";
|
|
36117
|
+
}
|
|
36118
|
+
function telemetryBucketCustomTools(n) {
|
|
36119
|
+
if (n <= 0) return "0";
|
|
36120
|
+
if (n === 1) return "1";
|
|
36121
|
+
if (n <= 3) return "2_3";
|
|
36122
|
+
if (n <= 6) return "4_6";
|
|
36123
|
+
if (n <= 12) return "7_12";
|
|
36124
|
+
return "13_plus";
|
|
36125
|
+
}
|
|
36126
|
+
function telemetryBucketMcp(n) {
|
|
36127
|
+
if (n <= 0) return "0";
|
|
36128
|
+
if (n === 1) return "1";
|
|
36129
|
+
if (n <= 3) return "2_3";
|
|
36130
|
+
return "4_plus";
|
|
36131
|
+
}
|
|
36132
|
+
function telemetryIntegration(opts) {
|
|
36133
|
+
const nMcp = opts.mcpServers?.length ?? 0;
|
|
36134
|
+
if (nMcp > 0) {
|
|
36135
|
+
return { integration: "mcp", integrationKind: "mcp", mcpBucket: telemetryBucketMcp(nMcp) };
|
|
36136
|
+
}
|
|
36137
|
+
if (opts.consult) {
|
|
36138
|
+
let isOpenclaw = false;
|
|
36139
|
+
const oc = opts.consult.openaiCompatible;
|
|
36140
|
+
if (oc) {
|
|
36141
|
+
const model = oc.model ?? "";
|
|
36142
|
+
const baseUrl = oc.baseUrl ?? "";
|
|
36143
|
+
isOpenclaw = model.startsWith("openclaw/") || baseUrl.includes(":18789");
|
|
36144
|
+
}
|
|
36145
|
+
return {
|
|
36146
|
+
integration: isOpenclaw ? "openclaw" : "other",
|
|
36147
|
+
integrationKind: "consult",
|
|
36148
|
+
mcpBucket: "0"
|
|
36149
|
+
};
|
|
36150
|
+
}
|
|
36151
|
+
return { integration: "none", integrationKind: "none", mcpBucket: "0" };
|
|
36152
|
+
}
|
|
36153
|
+
function telemetryEnvironmentDims() {
|
|
36154
|
+
try {
|
|
36155
|
+
const dims = {
|
|
36156
|
+
invoked_by_agent: invokedByAgent(),
|
|
36157
|
+
container: inContainer(),
|
|
36158
|
+
serverless: serverless(),
|
|
36159
|
+
cloud: cloud(),
|
|
36160
|
+
package_manager: packageManager(),
|
|
36161
|
+
days_since_install_bucket: daysSinceInstallBucket()
|
|
36162
|
+
};
|
|
36163
|
+
const prev = previousVersion(VERSION);
|
|
36164
|
+
if (prev) dims.previous_sdk_version = prev;
|
|
36165
|
+
return dims;
|
|
36166
|
+
} catch {
|
|
36167
|
+
return {};
|
|
36168
|
+
}
|
|
36169
|
+
}
|
|
34898
36170
|
var Patter = class {
|
|
34899
36171
|
localConfig;
|
|
34900
36172
|
embeddedServer = null;
|
|
@@ -34915,6 +36187,14 @@ var Patter = class {
|
|
|
34915
36187
|
* ``Cannot use both tunnel: true and webhookUrl``.
|
|
34916
36188
|
*/
|
|
34917
36189
|
tunnelOwnsWebhookUrl = false;
|
|
36190
|
+
/**
|
|
36191
|
+
* Anonymous usage telemetry (opt-out, default ON). Separate from
|
|
36192
|
+
* ``./observability`` (user-facing OTel). Fire-and-forget and fail-safe — it
|
|
36193
|
+
* can never block or break a call. See ``./telemetry``.
|
|
36194
|
+
*/
|
|
36195
|
+
telemetry;
|
|
36196
|
+
telemetrySeenEngines = /* @__PURE__ */ new Set();
|
|
36197
|
+
telemetrySeenAgentShapes = /* @__PURE__ */ new Set();
|
|
34918
36198
|
/**
|
|
34919
36199
|
* Pre-rendered first-message TTS audio per outbound call_id. Populated
|
|
34920
36200
|
* by :meth:`call` when ``agent.prewarmFirstMessage`` is true; consumed
|
|
@@ -35120,6 +36400,22 @@ var Patter = class {
|
|
|
35120
36400
|
openaiKey: options.openaiKey,
|
|
35121
36401
|
persistRoot: resolvePersistRoot(options.persist)
|
|
35122
36402
|
};
|
|
36403
|
+
this.telemetry = new TelemetryClient({
|
|
36404
|
+
sdkVersion: VERSION,
|
|
36405
|
+
flag: options.telemetry
|
|
36406
|
+
});
|
|
36407
|
+
const initDims = {
|
|
36408
|
+
carrier: carrierFamily2(carrier),
|
|
36409
|
+
tunnel: tunnel instanceof Static ? "static" : options.tunnel ? "configured" : "none",
|
|
36410
|
+
...telemetryEnvironmentDims()
|
|
36411
|
+
};
|
|
36412
|
+
if (this.telemetry.enabled) {
|
|
36413
|
+
try {
|
|
36414
|
+
if (isFirstRun()) this.telemetry.record("first_run", initDims);
|
|
36415
|
+
} catch {
|
|
36416
|
+
}
|
|
36417
|
+
}
|
|
36418
|
+
this.telemetry.record("sdk_initialized", initDims);
|
|
35123
36419
|
this._tunnelReady = new Promise((resolve2, reject) => {
|
|
35124
36420
|
this._tunnelReadyResolve = resolve2;
|
|
35125
36421
|
this._tunnelReadyReject = reject;
|
|
@@ -35139,6 +36435,55 @@ var Patter = class {
|
|
|
35139
36435
|
// === Agent definition ===
|
|
35140
36436
|
/** Resolve user-supplied agent options against engine defaults and return the merged config. */
|
|
35141
36437
|
agent(opts) {
|
|
36438
|
+
const family = telemetryEngineFamily(opts);
|
|
36439
|
+
const stack = stackDimensions(opts.stt, opts.tts, opts.llm);
|
|
36440
|
+
const featureKey = family + "|" + Object.entries(stack).sort().map(([k, v]) => `${k}=${v}`).join(",");
|
|
36441
|
+
if (!this.telemetrySeenEngines.has(featureKey)) {
|
|
36442
|
+
this.telemetrySeenEngines.add(featureKey);
|
|
36443
|
+
this.telemetry.record("feature_used", {
|
|
36444
|
+
engine: family,
|
|
36445
|
+
provider: telemetryProviderFamily(family),
|
|
36446
|
+
...stack
|
|
36447
|
+
});
|
|
36448
|
+
}
|
|
36449
|
+
const builtin = opts.consult ? 1 : 0;
|
|
36450
|
+
const customBucket = telemetryBucketCustomTools(opts.tools?.length ?? 0);
|
|
36451
|
+
const { integration, integrationKind, mcpBucket } = telemetryIntegration(opts);
|
|
36452
|
+
const engineObj = opts.engine;
|
|
36453
|
+
const nr = opts.openaiRealtimeNoiseReduction ?? engineObj?.noiseReduction;
|
|
36454
|
+
const noiseReduction = nr === "near_field" || nr === "far_field" ? nr : "none";
|
|
36455
|
+
const td = opts.realtimeTurnDetection ?? engineObj?.turnDetection;
|
|
36456
|
+
const turnDetection = td != null ? "custom" : "none";
|
|
36457
|
+
const preamblesUsed = Boolean(opts.toolCallPreambles);
|
|
36458
|
+
const perToolTimeoutsSet = Array.isArray(opts.tools) && opts.tools.some((t) => t.timeoutMs !== void 0);
|
|
36459
|
+
const llmFallbackConfigured = typeof opts.llm?.getAvailability === "function";
|
|
36460
|
+
const shapeKey = [
|
|
36461
|
+
builtin,
|
|
36462
|
+
customBucket,
|
|
36463
|
+
integration,
|
|
36464
|
+
integrationKind,
|
|
36465
|
+
mcpBucket,
|
|
36466
|
+
noiseReduction,
|
|
36467
|
+
turnDetection,
|
|
36468
|
+
preamblesUsed,
|
|
36469
|
+
perToolTimeoutsSet,
|
|
36470
|
+
llmFallbackConfigured
|
|
36471
|
+
].join("|");
|
|
36472
|
+
if (!this.telemetrySeenAgentShapes.has(shapeKey)) {
|
|
36473
|
+
this.telemetrySeenAgentShapes.add(shapeKey);
|
|
36474
|
+
this.telemetry.record("agent_configured", {
|
|
36475
|
+
builtin_tool_count: builtin,
|
|
36476
|
+
custom_tool_count_bucket: customBucket,
|
|
36477
|
+
integration,
|
|
36478
|
+
integration_kind: integrationKind,
|
|
36479
|
+
mcp_server_count_bucket: mcpBucket,
|
|
36480
|
+
noise_reduction: noiseReduction,
|
|
36481
|
+
turn_detection: turnDetection,
|
|
36482
|
+
preambles_used: preamblesUsed,
|
|
36483
|
+
per_tool_timeouts_set: perToolTimeoutsSet,
|
|
36484
|
+
llm_fallback_configured: llmFallbackConfigured
|
|
36485
|
+
});
|
|
36486
|
+
}
|
|
35142
36487
|
let working = { ...opts };
|
|
35143
36488
|
if (opts.engine) {
|
|
35144
36489
|
if (opts.provider) {
|
|
@@ -35328,6 +36673,7 @@ var Patter = class {
|
|
|
35328
36673
|
opts.dashboardToken ?? "",
|
|
35329
36674
|
opts.allowInsecureDashboard ?? false
|
|
35330
36675
|
);
|
|
36676
|
+
this.embeddedServer.telemetry = this.telemetry;
|
|
35331
36677
|
this.embeddedServer.popPrewarmAudio = this.popPrewarmAudio;
|
|
35332
36678
|
this.embeddedServer.popPrewarmedConnections = this.popPrewarmedConnections;
|
|
35333
36679
|
this.embeddedServer.recordPrewarmWaste = this.recordPrewarmWaste;
|
|
@@ -35337,6 +36683,7 @@ var Patter = class {
|
|
|
35337
36683
|
await waitForTunnelPubliclyReachable(webhookUrl);
|
|
35338
36684
|
}
|
|
35339
36685
|
this._readyResolve(webhookUrl);
|
|
36686
|
+
this.telemetry.flushPending();
|
|
35340
36687
|
} catch (err) {
|
|
35341
36688
|
const e = err instanceof Error ? err : new Error(String(err));
|
|
35342
36689
|
this._readyReject(e);
|
|
@@ -36019,6 +37366,7 @@ var Patter = class {
|
|
|
36019
37366
|
* entries leak across ``serve`` / ``disconnect`` cycles. See FIX #93.
|
|
36020
37367
|
*/
|
|
36021
37368
|
async disconnect() {
|
|
37369
|
+
this.telemetry.flushPending();
|
|
36022
37370
|
for (const handle of this.prewarmTtlTimers.values()) {
|
|
36023
37371
|
clearTimeout(handle);
|
|
36024
37372
|
}
|
|
@@ -36620,6 +37968,7 @@ var PatterTool = class {
|
|
|
36620
37968
|
maxDurationSec;
|
|
36621
37969
|
recording;
|
|
36622
37970
|
started = false;
|
|
37971
|
+
hermesTelemetryEmitted = false;
|
|
36623
37972
|
/** Cached in-progress (or completed) start promise so concurrent execute()
|
|
36624
37973
|
* callers all await the same boot sequence instead of each racing into
|
|
36625
37974
|
* phone.serve(). Reset to null on failure so callers can retry after a
|
|
@@ -36776,6 +38125,20 @@ var PatterTool = class {
|
|
|
36776
38125
|
* the same wire contract.
|
|
36777
38126
|
*/
|
|
36778
38127
|
hermesHandler() {
|
|
38128
|
+
if (!this.hermesTelemetryEmitted) {
|
|
38129
|
+
this.hermesTelemetryEmitted = true;
|
|
38130
|
+
try {
|
|
38131
|
+
const tel = this.phone.telemetry;
|
|
38132
|
+
tel?.record("agent_configured", {
|
|
38133
|
+
builtin_tool_count: 0,
|
|
38134
|
+
custom_tool_count_bucket: "0",
|
|
38135
|
+
integration: "hermes",
|
|
38136
|
+
integration_kind: "none",
|
|
38137
|
+
mcp_server_count_bucket: "0"
|
|
38138
|
+
});
|
|
38139
|
+
} catch {
|
|
38140
|
+
}
|
|
38141
|
+
}
|
|
36779
38142
|
return async (args) => {
|
|
36780
38143
|
try {
|
|
36781
38144
|
const result = await this.execute(args);
|
|
@@ -41868,11 +43231,16 @@ var LLM5 = class extends GoogleLLMProvider {
|
|
|
41868
43231
|
|
|
41869
43232
|
// src/llm/openai-compatible.ts
|
|
41870
43233
|
init_cjs_shims();
|
|
43234
|
+
var import_node_crypto6 = require("crypto");
|
|
41871
43235
|
init_llm_loop();
|
|
41872
43236
|
init_errors();
|
|
41873
43237
|
init_logger();
|
|
41874
43238
|
init_version();
|
|
41875
43239
|
var DEFAULT_TIMEOUT_S = 60;
|
|
43240
|
+
function hashCaller(caller) {
|
|
43241
|
+
if (!caller) return void 0;
|
|
43242
|
+
return (0, import_node_crypto6.createHash)("sha256").update(caller, "utf8").digest("hex").slice(0, 16);
|
|
43243
|
+
}
|
|
41876
43244
|
var OpenAICompatibleLLMProvider = class {
|
|
41877
43245
|
/**
|
|
41878
43246
|
* Stable pricing/dashboard key — read by stream-handler/metrics. Typed as
|
|
@@ -41891,6 +43259,7 @@ var OpenAICompatibleLLMProvider = class {
|
|
|
41891
43259
|
sessionIdPrefix;
|
|
41892
43260
|
sessionKeyHeader;
|
|
41893
43261
|
sessionKey;
|
|
43262
|
+
sessionKeyFactory;
|
|
41894
43263
|
temperature;
|
|
41895
43264
|
maxTokens;
|
|
41896
43265
|
responseFormat;
|
|
@@ -41920,6 +43289,17 @@ var OpenAICompatibleLLMProvider = class {
|
|
|
41920
43289
|
this.sessionIdPrefix = options.sessionIdPrefix;
|
|
41921
43290
|
this.sessionKeyHeader = options.sessionKeyHeader;
|
|
41922
43291
|
this.sessionKey = options.sessionKey;
|
|
43292
|
+
let sessionKeyFactory = options.sessionKeyFactory;
|
|
43293
|
+
if (!sessionKeyFactory && options.sessionKeyFrom === "caller_hash") {
|
|
43294
|
+
sessionKeyFactory = (ctx) => ctx.callerHash ? `patter-caller-${ctx.callerHash}` : void 0;
|
|
43295
|
+
} else if (options.sessionKeyFrom !== void 0 && options.sessionKeyFrom !== "caller_hash") {
|
|
43296
|
+
throw new Error(
|
|
43297
|
+
`sessionKeyFrom must be 'caller_hash' or undefined, got ${JSON.stringify(
|
|
43298
|
+
options.sessionKeyFrom
|
|
43299
|
+
)}`
|
|
43300
|
+
);
|
|
43301
|
+
}
|
|
43302
|
+
this.sessionKeyFactory = sessionKeyFactory;
|
|
41923
43303
|
this.temperature = options.temperature;
|
|
41924
43304
|
this.maxTokens = options.maxTokens;
|
|
41925
43305
|
this.responseFormat = options.responseFormat;
|
|
@@ -41943,7 +43323,7 @@ var OpenAICompatibleLLMProvider = class {
|
|
|
41943
43323
|
* - ``sessionKeyHeader`` (+ ``sessionKey``) → the static ``sessionKey`` value.
|
|
41944
43324
|
* ``sessionKey`` is a credential-grade memory scope and is never logged.
|
|
41945
43325
|
*/
|
|
41946
|
-
buildHeaders(callId) {
|
|
43326
|
+
buildHeaders(callId, caller, callee) {
|
|
41947
43327
|
const headers = {
|
|
41948
43328
|
"Content-Type": "application/json",
|
|
41949
43329
|
"User-Agent": `getpatter/${VERSION}`,
|
|
@@ -41955,11 +43335,33 @@ var OpenAICompatibleLLMProvider = class {
|
|
|
41955
43335
|
if (this.sessionIdHeader && callId) {
|
|
41956
43336
|
headers[this.sessionIdHeader] = `${this.sessionIdPrefix ?? ""}${callId}`;
|
|
41957
43337
|
}
|
|
41958
|
-
if (this.sessionKeyHeader
|
|
41959
|
-
|
|
43338
|
+
if (this.sessionKeyHeader) {
|
|
43339
|
+
const sessionKeyValue = this.resolveSessionKey(callId, caller, callee);
|
|
43340
|
+
if (sessionKeyValue) {
|
|
43341
|
+
headers[this.sessionKeyHeader] = sessionKeyValue;
|
|
43342
|
+
}
|
|
41960
43343
|
}
|
|
41961
43344
|
return headers;
|
|
41962
43345
|
}
|
|
43346
|
+
/**
|
|
43347
|
+
* Resolve the ``sessionKeyHeader`` VALUE for this call. When a
|
|
43348
|
+
* ``sessionKeyFactory`` is configured it is called with a
|
|
43349
|
+
* {@link SessionContext} (the raw ``caller`` plus its non-reversible
|
|
43350
|
+
* {@link hashCaller}) and its return value wins — a falsy return omits the
|
|
43351
|
+
* header. Otherwise the static ``sessionKey`` is used. Never logged.
|
|
43352
|
+
*/
|
|
43353
|
+
resolveSessionKey(callId, caller, callee) {
|
|
43354
|
+
if (this.sessionKeyFactory) {
|
|
43355
|
+
const ctx = {
|
|
43356
|
+
callId,
|
|
43357
|
+
caller,
|
|
43358
|
+
callee,
|
|
43359
|
+
callerHash: hashCaller(caller)
|
|
43360
|
+
};
|
|
43361
|
+
return this.sessionKeyFactory(ctx);
|
|
43362
|
+
}
|
|
43363
|
+
return this.sessionKey;
|
|
43364
|
+
}
|
|
41963
43365
|
/**
|
|
41964
43366
|
* Pre-call DNS / TLS warmup for the configured endpoint. Best-effort:
|
|
41965
43367
|
* 5 s timeout, all exceptions swallowed at debug level. The ``Authorization``
|
|
@@ -42013,10 +43415,12 @@ var OpenAICompatibleLLMProvider = class {
|
|
|
42013
43415
|
/** Stream Patter-format LLM chunks from the configured chat completions API. */
|
|
42014
43416
|
async *stream(messages, tools, opts) {
|
|
42015
43417
|
const callId = opts?.callId;
|
|
43418
|
+
const caller = opts?.caller;
|
|
43419
|
+
const callee = opts?.callee;
|
|
42016
43420
|
const body = this.buildBody(messages, tools, callId);
|
|
42017
43421
|
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
42018
43422
|
method: "POST",
|
|
42019
|
-
headers: this.buildHeaders(callId),
|
|
43423
|
+
headers: this.buildHeaders(callId, caller, callee),
|
|
42020
43424
|
body: JSON.stringify(body),
|
|
42021
43425
|
signal: mergeAbortSignals(opts?.signal, AbortSignal.timeout(this.timeoutMs))
|
|
42022
43426
|
});
|
|
@@ -42036,6 +43440,13 @@ var LLM6 = class extends OpenAICompatibleLLMProvider {
|
|
|
42036
43440
|
static providerKey = "openai_compatible";
|
|
42037
43441
|
};
|
|
42038
43442
|
|
|
43443
|
+
// src/llm/custom.ts
|
|
43444
|
+
init_cjs_shims();
|
|
43445
|
+
var LLM7 = class extends OpenAICompatibleLLMProvider {
|
|
43446
|
+
/** Stable pricing/dashboard key — read by stream-handler/metrics. */
|
|
43447
|
+
static providerKey = "custom";
|
|
43448
|
+
};
|
|
43449
|
+
|
|
42039
43450
|
// src/llm/hermes.ts
|
|
42040
43451
|
init_cjs_shims();
|
|
42041
43452
|
var BASE_URL = "http://127.0.0.1:8642/v1";
|
|
@@ -42047,7 +43458,7 @@ var SESSION_ID_HEADER = "X-Hermes-Session-Id";
|
|
|
42047
43458
|
var SESSION_ID_PREFIX = "patter-call-";
|
|
42048
43459
|
var SESSION_KEY_HEADER = "X-Hermes-Session-Key";
|
|
42049
43460
|
var DEFAULT_TIMEOUT_S2 = 120;
|
|
42050
|
-
var
|
|
43461
|
+
var LLM8 = class extends OpenAICompatibleLLMProvider {
|
|
42051
43462
|
static providerKey = "hermes";
|
|
42052
43463
|
constructor(opts = {}) {
|
|
42053
43464
|
const model = opts.model ?? process.env[MODEL_ENV] ?? DEFAULT_MODEL5;
|
|
@@ -42062,6 +43473,8 @@ var LLM7 = class extends OpenAICompatibleLLMProvider {
|
|
|
42062
43473
|
sessionIdPrefix: SESSION_ID_PREFIX,
|
|
42063
43474
|
sessionKeyHeader: SESSION_KEY_HEADER,
|
|
42064
43475
|
sessionKey: opts.sessionKey,
|
|
43476
|
+
sessionKeyFrom: opts.sessionKeyFrom,
|
|
43477
|
+
sessionKeyFactory: opts.sessionKeyFactory,
|
|
42065
43478
|
extraHeaders: opts.extraHeaders,
|
|
42066
43479
|
temperature: opts.temperature,
|
|
42067
43480
|
maxTokens: opts.maxTokens,
|
|
@@ -42086,7 +43499,7 @@ var SESSION_HEADER = "x-openclaw-session-key";
|
|
|
42086
43499
|
var SESSION_USER_PREFIX2 = "patter-call-";
|
|
42087
43500
|
var DEFAULT_TIMEOUT_S3 = 120;
|
|
42088
43501
|
var OPENCLAW_AGENT_RE2 = /^[A-Za-z0-9._:/-]+$/;
|
|
42089
|
-
var
|
|
43502
|
+
var LLM9 = class extends OpenAICompatibleLLMProvider {
|
|
42090
43503
|
static providerKey = "openclaw";
|
|
42091
43504
|
constructor(opts) {
|
|
42092
43505
|
const agent = opts?.agent;
|
|
@@ -42360,57 +43773,6 @@ var KrispVivaFilter = class {
|
|
|
42360
43773
|
}
|
|
42361
43774
|
};
|
|
42362
43775
|
|
|
42363
|
-
// src/telephony/twilio.ts
|
|
42364
|
-
init_cjs_shims();
|
|
42365
|
-
var Carrier2 = class {
|
|
42366
|
-
kind = "twilio";
|
|
42367
|
-
accountSid;
|
|
42368
|
-
authToken;
|
|
42369
|
-
constructor(opts = {}) {
|
|
42370
|
-
const sid = opts.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
|
|
42371
|
-
const tok = opts.authToken ?? process.env.TWILIO_AUTH_TOKEN;
|
|
42372
|
-
if (!sid) {
|
|
42373
|
-
throw new Error(
|
|
42374
|
-
"Twilio carrier requires accountSid. Pass { accountSid: 'AC...' } or set TWILIO_ACCOUNT_SID in the environment."
|
|
42375
|
-
);
|
|
42376
|
-
}
|
|
42377
|
-
if (!tok) {
|
|
42378
|
-
throw new Error(
|
|
42379
|
-
"Twilio carrier requires authToken. Pass { authToken: '...' } or set TWILIO_AUTH_TOKEN in the environment."
|
|
42380
|
-
);
|
|
42381
|
-
}
|
|
42382
|
-
this.accountSid = sid;
|
|
42383
|
-
this.authToken = tok;
|
|
42384
|
-
}
|
|
42385
|
-
};
|
|
42386
|
-
|
|
42387
|
-
// src/telephony/telnyx.ts
|
|
42388
|
-
init_cjs_shims();
|
|
42389
|
-
var Carrier3 = class {
|
|
42390
|
-
kind = "telnyx";
|
|
42391
|
-
apiKey;
|
|
42392
|
-
connectionId;
|
|
42393
|
-
publicKey;
|
|
42394
|
-
constructor(opts = {}) {
|
|
42395
|
-
const key = opts.apiKey ?? process.env.TELNYX_API_KEY;
|
|
42396
|
-
const conn = opts.connectionId ?? process.env.TELNYX_CONNECTION_ID;
|
|
42397
|
-
const pub = opts.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
|
|
42398
|
-
if (!key) {
|
|
42399
|
-
throw new Error(
|
|
42400
|
-
"Telnyx carrier requires apiKey. Pass { apiKey: '...' } or set TELNYX_API_KEY in the environment."
|
|
42401
|
-
);
|
|
42402
|
-
}
|
|
42403
|
-
if (!conn) {
|
|
42404
|
-
throw new Error(
|
|
42405
|
-
"Telnyx carrier requires connectionId. Pass { connectionId: '...' } or set TELNYX_CONNECTION_ID in the environment."
|
|
42406
|
-
);
|
|
42407
|
-
}
|
|
42408
|
-
this.apiKey = key;
|
|
42409
|
-
this.connectionId = conn;
|
|
42410
|
-
this.publicKey = pub;
|
|
42411
|
-
}
|
|
42412
|
-
};
|
|
42413
|
-
|
|
42414
43776
|
// src/index.ts
|
|
42415
43777
|
init_plivo();
|
|
42416
43778
|
init_openai_realtime_2();
|
|
@@ -42485,9 +43847,9 @@ init_tunnel();
|
|
|
42485
43847
|
|
|
42486
43848
|
// src/chat-context.ts
|
|
42487
43849
|
init_cjs_shims();
|
|
42488
|
-
var
|
|
43850
|
+
var import_node_crypto7 = require("crypto");
|
|
42489
43851
|
function generateId() {
|
|
42490
|
-
return (0,
|
|
43852
|
+
return (0, import_node_crypto7.randomUUID)().replace(/-/g, "").slice(0, 12);
|
|
42491
43853
|
}
|
|
42492
43854
|
function createMessage(role, content, options) {
|
|
42493
43855
|
return Object.freeze({
|
|
@@ -43136,8 +44498,8 @@ var TwilioAdapter = class _TwilioAdapter {
|
|
|
43136
44498
|
this.baseUrl = opts.region ? `https://api.${opts.region}.twilio.com/2010-04-01` : TWILIO_API_BASE2;
|
|
43137
44499
|
this.authHeader = `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`;
|
|
43138
44500
|
}
|
|
43139
|
-
async request(method,
|
|
43140
|
-
const url2 = `${this.baseUrl}/Accounts/${encodeURIComponent(this.accountSid)}${
|
|
44501
|
+
async request(method, path7, body) {
|
|
44502
|
+
const url2 = `${this.baseUrl}/Accounts/${encodeURIComponent(this.accountSid)}${path7}`;
|
|
43141
44503
|
const headers = { Authorization: this.authHeader };
|
|
43142
44504
|
if (body) headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
43143
44505
|
const response = await fetch(url2, {
|
|
@@ -43148,7 +44510,7 @@ var TwilioAdapter = class _TwilioAdapter {
|
|
|
43148
44510
|
});
|
|
43149
44511
|
const text = await response.text();
|
|
43150
44512
|
if (!response.ok) {
|
|
43151
|
-
throw new Error(`Twilio ${method} ${
|
|
44513
|
+
throw new Error(`Twilio ${method} ${path7} failed: ${response.status} ${text}`);
|
|
43152
44514
|
}
|
|
43153
44515
|
if (!text) return {};
|
|
43154
44516
|
try {
|
|
@@ -43166,8 +44528,8 @@ var TwilioAdapter = class _TwilioAdapter {
|
|
|
43166
44528
|
const country = encodeURIComponent(opts.countryCode);
|
|
43167
44529
|
const queryParts = ["PageSize=1"];
|
|
43168
44530
|
if (opts.areaCode) queryParts.push(`AreaCode=${encodeURIComponent(opts.areaCode)}`);
|
|
43169
|
-
const
|
|
43170
|
-
const available = await this.request("GET",
|
|
44531
|
+
const path7 = `/AvailablePhoneNumbers/${country}/Local.json?${queryParts.join("&")}`;
|
|
44532
|
+
const available = await this.request("GET", path7);
|
|
43171
44533
|
const first = available.available_phone_numbers?.[0]?.phone_number;
|
|
43172
44534
|
if (!first) {
|
|
43173
44535
|
throw new Error(`TwilioAdapter: no numbers available for country ${opts.countryCode}`);
|
|
@@ -43267,7 +44629,7 @@ var TwilioAdapter = class _TwilioAdapter {
|
|
|
43267
44629
|
|
|
43268
44630
|
// src/providers/telnyx-adapter.ts
|
|
43269
44631
|
init_cjs_shims();
|
|
43270
|
-
var
|
|
44632
|
+
var import_node_crypto8 = require("crypto");
|
|
43271
44633
|
init_logger();
|
|
43272
44634
|
var TELNYX_API_BASE2 = "https://api.telnyx.com/v2";
|
|
43273
44635
|
var TelnyxAdapter = class {
|
|
@@ -43279,8 +44641,8 @@ var TelnyxAdapter = class {
|
|
|
43279
44641
|
this.apiKey = apiKey;
|
|
43280
44642
|
this.connectionId = connectionId;
|
|
43281
44643
|
}
|
|
43282
|
-
async request(method,
|
|
43283
|
-
const url2 = `${this.baseUrl}${
|
|
44644
|
+
async request(method, path7, body) {
|
|
44645
|
+
const url2 = `${this.baseUrl}${path7}`;
|
|
43284
44646
|
const headers = {
|
|
43285
44647
|
Authorization: `Bearer ${this.apiKey}`
|
|
43286
44648
|
};
|
|
@@ -43293,7 +44655,7 @@ var TelnyxAdapter = class {
|
|
|
43293
44655
|
});
|
|
43294
44656
|
const text = await response.text();
|
|
43295
44657
|
if (!response.ok) {
|
|
43296
|
-
throw new Error(`Telnyx ${method} ${
|
|
44658
|
+
throw new Error(`Telnyx ${method} ${path7} failed: ${response.status} ${text}`);
|
|
43297
44659
|
}
|
|
43298
44660
|
if (!text) return {};
|
|
43299
44661
|
try {
|
|
@@ -43377,7 +44739,7 @@ var TelnyxAdapter = class {
|
|
|
43377
44739
|
if (!callControlId) throw new Error("TelnyxAdapter: callControlId is required");
|
|
43378
44740
|
const encoded = encodeURIComponent(callControlId);
|
|
43379
44741
|
const body = {
|
|
43380
|
-
command_id: opts.commandId ?? (0,
|
|
44742
|
+
command_id: opts.commandId ?? (0, import_node_crypto8.randomUUID)()
|
|
43381
44743
|
};
|
|
43382
44744
|
try {
|
|
43383
44745
|
await this.request(
|
|
@@ -43658,6 +45020,12 @@ var TelnyxTTS = class {
|
|
|
43658
45020
|
init_cjs_shims();
|
|
43659
45021
|
init_tracing();
|
|
43660
45022
|
init_event_bus();
|
|
45023
|
+
|
|
45024
|
+
// src/index.ts
|
|
45025
|
+
var hermes = Object.freeze({ LLM: LLM8 });
|
|
45026
|
+
var openclaw = Object.freeze({ LLM: LLM9 });
|
|
45027
|
+
var openaiCompatible = Object.freeze({ LLM: LLM6 });
|
|
45028
|
+
var custom2 = Object.freeze({ LLM: LLM7 });
|
|
43661
45029
|
// Annotate the CommonJS export names for ESM import in node:
|
|
43662
45030
|
0 && (module.exports = {
|
|
43663
45031
|
AllProvidersFailedError,
|
|
@@ -43674,6 +45042,7 @@ init_event_bus();
|
|
|
43674
45042
|
CerebrasLLM,
|
|
43675
45043
|
ChatContext,
|
|
43676
45044
|
CloudflareTunnel,
|
|
45045
|
+
CustomLLM,
|
|
43677
45046
|
DEFAULT_MIN_SENTENCE_LEN,
|
|
43678
45047
|
DEFAULT_PRICING,
|
|
43679
45048
|
DTMF_EVENTS,
|
|
@@ -43792,6 +45161,7 @@ init_event_bus();
|
|
|
43792
45161
|
createResampler24kTo16k,
|
|
43793
45162
|
createResampler24kTo8k,
|
|
43794
45163
|
createResampler8kTo16k,
|
|
45164
|
+
custom,
|
|
43795
45165
|
deepgram,
|
|
43796
45166
|
defineTool,
|
|
43797
45167
|
elevenlabs,
|
|
@@ -43803,6 +45173,8 @@ init_event_bus();
|
|
|
43803
45173
|
geminiLive,
|
|
43804
45174
|
getLogger,
|
|
43805
45175
|
guardrail,
|
|
45176
|
+
hashCaller,
|
|
45177
|
+
hermes,
|
|
43806
45178
|
initTracing,
|
|
43807
45179
|
isRemoteUrl,
|
|
43808
45180
|
isTracingEnabled,
|
|
@@ -43815,7 +45187,9 @@ init_event_bus();
|
|
|
43815
45187
|
mountDashboard,
|
|
43816
45188
|
mulawToPcm16,
|
|
43817
45189
|
notifyDashboard,
|
|
45190
|
+
openaiCompatible,
|
|
43818
45191
|
openaiTts,
|
|
45192
|
+
openclaw,
|
|
43819
45193
|
openclawConsult,
|
|
43820
45194
|
openclawPostCallNotifier,
|
|
43821
45195
|
pcm16ToMulaw,
|