getpatter 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  import {
5
5
  AuthenticationError,
6
6
  CallMetricsAccumulator,
7
+ Carrier,
7
8
  DEFAULT_MIN_SENTENCE_LEN,
8
9
  DEFAULT_PRICING,
9
10
  DeepgramModel,
@@ -21,6 +22,7 @@ import {
21
22
  PatterConnectionError,
22
23
  PatterError,
23
24
  PipelineHookExecutor,
25
+ PlivoAdapter,
24
26
  PricingUnit,
25
27
  ProvisionError,
26
28
  RateLimitError,
@@ -52,7 +54,7 @@ import {
52
54
  mountDashboard,
53
55
  resolveLogRoot,
54
56
  startSpan
55
- } from "./chunk-LE63CSOB.mjs";
57
+ } from "./chunk-Z6W5XFWS.mjs";
56
58
  import {
57
59
  OpenAIRealtime2Adapter,
58
60
  OpenAIRealtimeAdapter,
@@ -799,7 +801,7 @@ var Patter = class {
799
801
  }
800
802
  if (!options.carrier) {
801
803
  throw new Error(
802
- "Local mode requires a `carrier` instance. Pass `carrier: new Twilio({...})` or `carrier: new Telnyx({...})`."
804
+ "Local mode requires a `carrier` instance. Pass `carrier: new Twilio({...})`, `carrier: new Telnyx({...})` or `carrier: new Plivo({...})`."
803
805
  );
804
806
  }
805
807
  const carrier = options.carrier;
@@ -977,16 +979,18 @@ var Patter = class {
977
979
  throw err;
978
980
  }
979
981
  const carrier = this.localConfig.carrier;
980
- const telephonyProvider = carrier.kind === "twilio" ? "twilio" : "telnyx";
982
+ const telephonyProvider = carrier.kind;
981
983
  const wantsCarrierManagement = opts.manageWebhook !== false || wantsCloudflared;
982
984
  if (wantsCarrierManagement) {
983
- const { autoConfigureCarrier } = await import("./carrier-config-4ZKVYAWV.mjs");
985
+ const { autoConfigureCarrier } = await import("./carrier-config-3WDQXP5J.mjs");
984
986
  await autoConfigureCarrier({
985
987
  telephonyProvider,
986
988
  twilioSid: carrier.kind === "twilio" ? carrier.accountSid : void 0,
987
989
  twilioToken: carrier.kind === "twilio" ? carrier.authToken : void 0,
988
990
  telnyxKey: carrier.kind === "telnyx" ? carrier.apiKey : void 0,
989
991
  telnyxConnectionId: carrier.kind === "telnyx" ? carrier.connectionId : void 0,
992
+ plivoAuthId: carrier.kind === "plivo" ? carrier.authId : void 0,
993
+ plivoAuthToken: carrier.kind === "plivo" ? carrier.authToken : void 0,
990
994
  phoneNumber: this.localConfig.phoneNumber,
991
995
  webhookHost: webhookUrl
992
996
  });
@@ -1002,6 +1006,8 @@ var Patter = class {
1002
1006
  telnyxKey: carrier.kind === "telnyx" ? carrier.apiKey : void 0,
1003
1007
  telnyxConnectionId: carrier.kind === "telnyx" ? carrier.connectionId : void 0,
1004
1008
  telnyxPublicKey: carrier.kind === "telnyx" ? carrier.publicKey : void 0,
1009
+ plivoAuthId: carrier.kind === "plivo" ? carrier.authId : void 0,
1010
+ plivoAuthToken: carrier.kind === "plivo" ? carrier.authToken : void 0,
1005
1011
  persistRoot: this.localConfig.persistRoot
1006
1012
  },
1007
1013
  opts.agent,
@@ -1033,7 +1039,7 @@ var Patter = class {
1033
1039
  }
1034
1040
  /** Run the agent in interactive terminal-test mode (no real telephony). */
1035
1041
  async test(opts) {
1036
- const { TestSession: TestSession2 } = await import("./test-mode-RS57BDM6.mjs");
1042
+ const { TestSession: TestSession2 } = await import("./test-mode-MDBQ4ECE.mjs");
1037
1043
  const session = new TestSession2();
1038
1044
  await session.run({
1039
1045
  agent: opts.agent,
@@ -1413,7 +1419,15 @@ var Patter = class {
1413
1419
  this.prewarmTtlTimers.set(callId, handle);
1414
1420
  });
1415
1421
  }
1416
- /** Place an outbound call via the configured carrier. */
1422
+ /**
1423
+ * Place an outbound call via the configured carrier.
1424
+ *
1425
+ * With `wait: false` (default) this resolves to `void` the instant the
1426
+ * carrier accepts the dial (fire-and-forget). With `wait: true` it blocks
1427
+ * until the call reaches a terminal state and resolves to a
1428
+ * {@link CallResult} — see {@link LocalCallOptions.wait}. Mirrors Python's
1429
+ * `Patter.call(..., wait=False)`.
1430
+ */
1417
1431
  async call(options) {
1418
1432
  if (!options.to) {
1419
1433
  throw new Error("'to' phone number is required");
@@ -1421,7 +1435,13 @@ var Patter = class {
1421
1435
  if (!options.to.startsWith("+")) {
1422
1436
  throw new Error(`'to' must be in E.164 format (e.g., '+1234567890'). Got: '${options.to}'`);
1423
1437
  }
1438
+ if (options.wait && !this.embeddedServer) {
1439
+ throw new PatterConnectionError(
1440
+ "call({ wait: true }) requires an active server to receive the carrier completion webhooks. Call `await phone.serve(...)` first, or use `await using phone = new Patter(...)` (and serve inside the block) which keeps the server up for the duration of the block."
1441
+ );
1442
+ }
1424
1443
  const { phoneNumber, webhookUrl, carrier } = this.localConfig;
1444
+ let callId = "";
1425
1445
  const effectiveRingTimeout = options.ringTimeout === void 0 ? 25 : options.ringTimeout;
1426
1446
  const wantsAmd = options.machineDetection !== false || Boolean(options.voicemailMessage);
1427
1447
  if (this.embeddedServer) {
@@ -1479,11 +1499,74 @@ var Patter = class {
1479
1499
  }
1480
1500
  }
1481
1501
  if (telnyxCallId) {
1502
+ callId = telnyxCallId;
1482
1503
  this.spawnPrewarmFirstMessage(options.agent, telnyxCallId, effectiveRingTimeout, "telnyx");
1483
1504
  if (options.agent.prewarm !== false) {
1484
1505
  this.parkProviderConnections(options.agent, telnyxCallId);
1485
1506
  }
1486
1507
  }
1508
+ return this.maybeAwaitCompletion(options, callId, effectiveRingTimeout);
1509
+ }
1510
+ if (carrier.kind === "plivo") {
1511
+ const auth = `Basic ${Buffer.from(`${carrier.authId}:${carrier.authToken}`).toString("base64")}`;
1512
+ const plivoPayload = {
1513
+ from: phoneNumber,
1514
+ to: options.to,
1515
+ answer_url: `https://${webhookUrl}/webhooks/plivo/voice`,
1516
+ answer_method: "POST",
1517
+ // hangup_url is Plivo's StatusCallback analogue — without it the
1518
+ // /webhooks/plivo/status route never fires for outbound calls and
1519
+ // the dashboard misses no-answer / busy / failed.
1520
+ hangup_url: `https://${webhookUrl}/webhooks/plivo/status`,
1521
+ hangup_method: "POST"
1522
+ };
1523
+ if (effectiveRingTimeout !== null && effectiveRingTimeout !== void 0) {
1524
+ plivoPayload.ring_timeout = Math.max(1, Math.floor(effectiveRingTimeout));
1525
+ }
1526
+ if (wantsAmd) {
1527
+ plivoPayload.machine_detection = "true";
1528
+ plivoPayload.machine_detection_time = 5e3;
1529
+ plivoPayload.machine_detection_url = `https://${webhookUrl}/webhooks/plivo/amd`;
1530
+ plivoPayload.machine_detection_method = "POST";
1531
+ }
1532
+ if (options.voicemailMessage && this.embeddedServer) {
1533
+ this.embeddedServer.voicemailMessage = options.voicemailMessage;
1534
+ }
1535
+ const response2 = await fetch(`https://api.plivo.com/v1/Account/${carrier.authId}/Call/`, {
1536
+ method: "POST",
1537
+ headers: { "Content-Type": "application/json", Authorization: auth },
1538
+ body: JSON.stringify(plivoPayload)
1539
+ });
1540
+ if (!response2.ok) {
1541
+ throw new ProvisionError(`Failed to initiate Plivo call: ${await response2.text()}`);
1542
+ }
1543
+ let plivoCallId;
1544
+ try {
1545
+ const body = await response2.clone().json();
1546
+ plivoCallId = body.request_uuid;
1547
+ } catch {
1548
+ }
1549
+ if (plivoCallId) {
1550
+ const initiatedPayload = {
1551
+ call_id: plivoCallId,
1552
+ caller: phoneNumber,
1553
+ callee: options.to,
1554
+ direction: "outbound",
1555
+ status: "initiated"
1556
+ };
1557
+ if (this.embeddedServer) {
1558
+ this.embeddedServer.metricsStore.recordCallInitiated(initiatedPayload);
1559
+ }
1560
+ try {
1561
+ const { notifyDashboard: notifyDashboard2 } = await import("./persistence-LVIAHESK.mjs");
1562
+ notifyDashboard2(initiatedPayload);
1563
+ } catch {
1564
+ }
1565
+ this.spawnPrewarmFirstMessage(options.agent, plivoCallId, effectiveRingTimeout, "plivo");
1566
+ if (options.agent.prewarm !== false) {
1567
+ this.parkProviderConnections(options.agent, plivoCallId);
1568
+ }
1569
+ }
1487
1570
  return;
1488
1571
  }
1489
1572
  const twilioSid = carrier.accountSid;
@@ -1555,11 +1638,53 @@ var Patter = class {
1555
1638
  }
1556
1639
  }
1557
1640
  if (twilioCallSid) {
1641
+ callId = twilioCallSid;
1558
1642
  this.spawnPrewarmFirstMessage(options.agent, twilioCallSid, effectiveRingTimeout, "twilio");
1559
1643
  if (options.agent.prewarm !== false) {
1560
1644
  this.parkProviderConnections(options.agent, twilioCallSid);
1561
1645
  }
1562
1646
  }
1647
+ return this.maybeAwaitCompletion(options, callId, effectiveRingTimeout);
1648
+ }
1649
+ /**
1650
+ * When `options.wait` is set, register a completion promise keyed by the
1651
+ * carrier-issued `callId` and await it (bounded by a backstop timeout).
1652
+ * Otherwise resolve to `void` immediately (fire-and-forget).
1653
+ *
1654
+ * The registration happens here — after the carrier accepted the dial and
1655
+ * issued the id — so the future correlates to the right call. The race
1656
+ * window between `initiateCall` returning and this registration is
1657
+ * harmless: the callee is still ringing, so no terminal signal can fire
1658
+ * before we register. Mirrors the Python `call(wait=True)` tail block.
1659
+ */
1660
+ async maybeAwaitCompletion(options, callId, ringTimeout) {
1661
+ if (!options.wait) return;
1662
+ const server = this.embeddedServer;
1663
+ if (!server || !callId) {
1664
+ throw new PatterConnectionError(
1665
+ "call({ wait: true }): no active server or carrier call id."
1666
+ );
1667
+ }
1668
+ const completion = server.registerCompletion(callId);
1669
+ const backstopMs = ((ringTimeout ?? 25) + 1800) * 1e3;
1670
+ let timer;
1671
+ const backstop = new Promise((_resolve, reject) => {
1672
+ timer = setTimeout(() => {
1673
+ server.deleteCompletion(callId);
1674
+ reject(
1675
+ new PatterConnectionError(
1676
+ `call({ wait: true }): no terminal signal for call ${callId} within ${(backstopMs / 1e3).toFixed(0)}s`,
1677
+ { code: ErrorCode.TIMEOUT }
1678
+ )
1679
+ );
1680
+ }, backstopMs);
1681
+ timer.unref?.();
1682
+ });
1683
+ try {
1684
+ return await Promise.race([completion, backstop]);
1685
+ } finally {
1686
+ if (timer) clearTimeout(timer);
1687
+ }
1563
1688
  }
1564
1689
  /**
1565
1690
  * Stop the embedded server and any running tunnel. Safe to call multiple
@@ -1600,6 +1725,11 @@ var Patter = class {
1600
1725
  this.tunnelHandle = null;
1601
1726
  }
1602
1727
  if (this.embeddedServer) {
1728
+ this.embeddedServer.failPendingCompletions(
1729
+ new PatterConnectionError(
1730
+ "Patter.disconnect() called while a call({ wait: true }) was still in flight."
1731
+ )
1732
+ );
1603
1733
  await this.embeddedServer.stop();
1604
1734
  this.embeddedServer = null;
1605
1735
  }
@@ -1623,6 +1753,30 @@ var Patter = class {
1623
1753
  this._ready.catch(() => {
1624
1754
  });
1625
1755
  }
1756
+ /**
1757
+ * Explicit-resource-management disposer so callers can write
1758
+ * ``await using phone = new Patter(...)`` and have {@link disconnect} run
1759
+ * automatically when the block exits — on the normal path AND when the
1760
+ * body throws. This guarantees the embedded server, any auto-started
1761
+ * tunnel, and in-flight prewarm/TTS work are torn down so a still-running
1762
+ * TTS WebSocket cannot keep the user billed after the block ends, and any
1763
+ * in-flight ``call({ wait: true })`` awaiter is failed rather than left
1764
+ * hanging. ``disconnect()`` is idempotent, so an explicit ``disconnect()``
1765
+ * inside the block is still safe. Mirrors Python's ``async with Patter(...)``.
1766
+ *
1767
+ * Note: this does NOT start the server (``serve()`` blocks until shutdown,
1768
+ * so it cannot run from a disposer) — call ``serve(...)`` inside the block:
1769
+ *
1770
+ * ```ts
1771
+ * await using phone = new Patter({ carrier: new Twilio(), phoneNumber: "+1555..." });
1772
+ * await phone.serve({ agent }); // inbound, or
1773
+ * const result = await phone.call({ to: "+1555...", agent, wait: true });
1774
+ * // disconnect() has run here — nothing left running.
1775
+ * ```
1776
+ */
1777
+ async [Symbol.asyncDispose]() {
1778
+ await this.disconnect();
1779
+ }
1626
1780
  /**
1627
1781
  * Terminate an active call on the configured carrier.
1628
1782
  *
@@ -1677,6 +1831,17 @@ var Patter = class {
1677
1831
  }
1678
1832
  return;
1679
1833
  }
1834
+ if (carrier.kind === "plivo") {
1835
+ const auth = Buffer.from(`${carrier.authId}:${carrier.authToken}`).toString("base64");
1836
+ const res = await fetch(
1837
+ `https://api.plivo.com/v1/Account/${carrier.authId}/Call/${encodeURIComponent(callSid)}/`,
1838
+ { method: "DELETE", headers: { Authorization: `Basic ${auth}` } }
1839
+ );
1840
+ if (!res.ok && res.status !== 404) {
1841
+ throw new Error(`Plivo hangup failed: ${res.status} ${await res.text()}`);
1842
+ }
1843
+ return;
1844
+ }
1680
1845
  throw new Error(`endCall() requires a configured carrier; got kind=${carrier.kind}`);
1681
1846
  }
1682
1847
  };
@@ -2074,7 +2239,6 @@ init_esm_shims();
2074
2239
 
2075
2240
  // src/integrations/patter-tool.ts
2076
2241
  init_esm_shims();
2077
- import { EventEmitter } from "events";
2078
2242
  var PARAMETERS_SCHEMA = {
2079
2243
  type: "object",
2080
2244
  properties: {
@@ -2101,7 +2265,7 @@ var PARAMETERS_SCHEMA = {
2101
2265
  };
2102
2266
  var DEFAULT_NAME = "make_phone_call";
2103
2267
  var DEFAULT_DESCRIPTION = "Place a real outbound phone call. Returns a JSON object with the full transcript, call status, duration in seconds, and cost. Use this when the user asks you to call someone, schedule appointments by phone, or otherwise reach a human via voice.";
2104
- var PatterTool = class _PatterTool {
2268
+ var PatterTool = class {
2105
2269
  name;
2106
2270
  description;
2107
2271
  phone;
@@ -2109,24 +2273,6 @@ var PatterTool = class _PatterTool {
2109
2273
  maxDurationSec;
2110
2274
  recording;
2111
2275
  started = false;
2112
- /** Resolver for the next `call_initiated` SSE event. Only set inside the
2113
- * dial mutex (`dialQueue`), so two parallel `execute()` calls never share
2114
- * it and never lose a dispatch. */
2115
- pendingDial = null;
2116
- /** Mutex that serializes the dial → call_id capture critical section.
2117
- * Each `execute()` chains a continuation onto this promise so the
2118
- * `pendingDial` slot is owned by exactly one caller at a time. */
2119
- dialQueue = Promise.resolve();
2120
- /** Captured SSE listener so `stop()` can detach it (prevents leaks when
2121
- * the underlying Patter instance outlives this tool). */
2122
- sseListener = null;
2123
- /** Captured Patter metrics store, for cleanup in `stop()`. */
2124
- metricsStoreRef = null;
2125
- /** call_id → pending promise machinery. */
2126
- pending = /* @__PURE__ */ new Map();
2127
- bus = new EventEmitter();
2128
- /** How long to wait for the `call_initiated` SSE before failing the dial. */
2129
- static DIAL_CAPTURE_TIMEOUT_MS = 1e4;
2130
2276
  constructor(opts) {
2131
2277
  if (!opts.phone) {
2132
2278
  throw new Error("PatterTool: `phone` (a Patter instance) is required.");
@@ -2170,7 +2316,15 @@ var PatterTool = class _PatterTool {
2170
2316
  };
2171
2317
  }
2172
2318
  // --- Lifecycle ----------------------------------------------------------
2173
- /** Start the underlying Patter server. Idempotent. */
2319
+ /**
2320
+ * Start the underlying Patter server. Idempotent.
2321
+ *
2322
+ * `execute()` relies on `Patter.call({ wait: true })`, which requires an
2323
+ * active server to receive the carrier completion webhooks — that's what
2324
+ * `serve()` provides here. No `onCallEnd` callback is wired: the SDK's own
2325
+ * per-callId completion registry resolves the result, so the user's
2326
+ * `onCallEnd` slot is left free.
2327
+ */
2174
2328
  async start() {
2175
2329
  if (this.started) return;
2176
2330
  if (!this.agent) {
@@ -2182,52 +2336,31 @@ var PatterTool = class _PatterTool {
2182
2336
  await this.phone.serve({
2183
2337
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
2184
2338
  agent: builtAgent,
2185
- recording: this.recording,
2186
- onCallEnd: this.onCallEndHandler.bind(this)
2339
+ recording: this.recording
2187
2340
  });
2188
- const store = this.phone.metricsStore;
2189
- if (!store) {
2190
- throw new Error(
2191
- "PatterTool.start: phone.metricsStore is null after serve() \u2014 is the dashboard disabled?"
2192
- );
2193
- }
2194
- const listener = (event) => {
2195
- if (event.type === "call_initiated" && this.pendingDial) {
2196
- const callId = event.data.call_id || "";
2197
- if (callId) {
2198
- const dispatch = this.pendingDial;
2199
- this.pendingDial = null;
2200
- dispatch(callId);
2201
- }
2202
- }
2203
- };
2204
- store.on("sse", listener);
2205
- this.sseListener = listener;
2206
- this.metricsStoreRef = store;
2207
2341
  this.started = true;
2208
2342
  }
2209
- /** Stop the underlying Patter server (and reject any pending calls). */
2343
+ /** Best-effort shutdown — tear the Patter server down via `disconnect()`. */
2210
2344
  async stop() {
2211
2345
  if (!this.started) return;
2212
- if (this.metricsStoreRef && this.sseListener) {
2213
- this.metricsStoreRef.off("sse", this.sseListener);
2214
- }
2215
- this.sseListener = null;
2216
- this.metricsStoreRef = null;
2217
- this.pendingDial = null;
2218
- for (const [, p] of this.pending) {
2219
- clearTimeout(p.timer);
2220
- p.reject(new Error("PatterTool: shutdown while call pending"));
2221
- }
2222
- this.pending.clear();
2223
- const stoppable = this.phone;
2224
- if (typeof stoppable.stop === "function") {
2225
- await stoppable.stop();
2346
+ const disconnectable = this.phone;
2347
+ if (typeof disconnectable.disconnect === "function") {
2348
+ try {
2349
+ await disconnectable.disconnect();
2350
+ } catch {
2351
+ }
2226
2352
  }
2227
2353
  this.started = false;
2228
2354
  }
2229
2355
  // --- Execution ----------------------------------------------------------
2230
- /** Place an outbound call and resolve once it ends with the transcript and metrics. */
2356
+ /**
2357
+ * Dial outbound, wait for the call to end, return a structured result.
2358
+ *
2359
+ * Thin wrapper over `Patter.call({ wait: true })`: the SDK now owns the
2360
+ * dial → callId → terminal-signal correlation, so this just bounds the wait
2361
+ * with `max_duration_sec` and maps the {@link CallResult} into the tool's
2362
+ * public envelope. Mirrors Python's `PatterTool.execute`.
2363
+ */
2231
2364
  async execute(args) {
2232
2365
  if (!this.started) await this.start();
2233
2366
  if (!args || typeof args.to !== "string" || !args.to.startsWith("+")) {
@@ -2243,55 +2376,32 @@ var PatterTool = class _PatterTool {
2243
2376
  ...args.goal !== void 0 ? { systemPrompt: args.goal } : {},
2244
2377
  ...args.first_message !== void 0 ? { firstMessage: args.first_message } : {}
2245
2378
  });
2246
- const callId = await this.acquireCallId(args.to, overrideAgent);
2247
- return new Promise((resolve, reject) => {
2248
- const timer = setTimeout(() => {
2249
- this.pending.delete(callId);
2250
- reject(new Error(`PatterTool.execute: call ${callId} exceeded ${timeoutSec}s timeout`));
2379
+ let timer;
2380
+ const timeout = new Promise((_resolve, reject) => {
2381
+ timer = setTimeout(() => {
2382
+ reject(
2383
+ new Error(
2384
+ `PatterTool.execute: call to ${args.to} exceeded ${timeoutSec}s timeout`
2385
+ )
2386
+ );
2251
2387
  }, timeoutSec * 1e3);
2252
- this.pending.set(callId, {
2253
- resolve,
2254
- reject,
2255
- timer,
2256
- startedAt: Date.now() / 1e3
2257
- });
2258
- });
2259
- }
2260
- /** Issue the outbound dial under the mutex and return its assigned call_id. */
2261
- async acquireCallId(to, agent) {
2262
- let release;
2263
- const slot = new Promise((r) => {
2264
- release = r;
2388
+ timer.unref?.();
2265
2389
  });
2266
- const previous = this.dialQueue;
2267
- this.dialQueue = previous.then(() => slot);
2268
- await previous;
2269
- let captureTimer = null;
2390
+ let result;
2270
2391
  try {
2271
- const callIdPromise = new Promise((resolve, reject) => {
2272
- this.pendingDial = resolve;
2273
- captureTimer = setTimeout(() => {
2274
- this.pendingDial = null;
2275
- reject(
2276
- new Error(
2277
- `PatterTool.execute: did not observe call_initiated within ${_PatterTool.DIAL_CAPTURE_TIMEOUT_MS}ms`
2278
- )
2279
- );
2280
- }, _PatterTool.DIAL_CAPTURE_TIMEOUT_MS);
2281
- });
2282
- await this.phone.call({
2283
- to,
2284
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2285
- agent
2286
- });
2287
- const callId = await callIdPromise;
2288
- if (captureTimer) clearTimeout(captureTimer);
2289
- return callId;
2392
+ result = await Promise.race([
2393
+ this.phone.call({
2394
+ to: args.to,
2395
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2396
+ agent: overrideAgent,
2397
+ wait: true
2398
+ }),
2399
+ timeout
2400
+ ]);
2290
2401
  } finally {
2291
- if (captureTimer) clearTimeout(captureTimer);
2292
- this.pendingDial = null;
2293
- release();
2402
+ if (timer) clearTimeout(timer);
2294
2403
  }
2404
+ return resultFromCallResult(result);
2295
2405
  }
2296
2406
  /**
2297
2407
  * Hermes-style handler: `(args, kwargs) => Promise<string>` returning a JSON
@@ -2309,32 +2419,32 @@ var PatterTool = class _PatterTool {
2309
2419
  }
2310
2420
  };
2311
2421
  }
2312
- // --- Internal: onCallEnd dispatcher -------------------------------------
2313
- async onCallEndHandler(data) {
2314
- const callId = data.call_id || "";
2315
- if (!callId) return;
2316
- const pending = this.pending.get(callId);
2317
- if (!pending) {
2318
- this.bus.emit("orphan_end", { call_id: callId, data });
2319
- return;
2320
- }
2321
- clearTimeout(pending.timer);
2322
- this.pending.delete(callId);
2323
- const metrics = data.metrics && typeof data.metrics === "object" ? data.metrics : null;
2324
- const cost = metrics && typeof metrics.cost === "object" && metrics.cost && typeof metrics.cost.total === "number" ? metrics.cost.total : void 0;
2325
- const duration = typeof metrics?.duration_seconds === "number" ? metrics?.duration_seconds : Math.max(0, Date.now() / 1e3 - pending.startedAt);
2326
- const transcript = Array.isArray(data.transcript) ? data.transcript : [];
2327
- const status = data.status || "completed";
2328
- pending.resolve({
2329
- call_id: callId,
2330
- status,
2331
- duration_seconds: duration,
2332
- cost_usd: cost,
2333
- transcript,
2334
- metrics
2335
- });
2336
- }
2337
2422
  };
2423
+ function resultFromCallResult(result) {
2424
+ if (!result) {
2425
+ return {
2426
+ call_id: "",
2427
+ status: "completed",
2428
+ outcome: "",
2429
+ duration_seconds: 0,
2430
+ cost_usd: void 0,
2431
+ transcript: [],
2432
+ metrics: null
2433
+ };
2434
+ }
2435
+ const costTotal = result.cost?.total;
2436
+ const costUsd = typeof costTotal === "number" ? costTotal : void 0;
2437
+ const metrics = result.metrics ? result.metrics : null;
2438
+ return {
2439
+ call_id: result.callId || "",
2440
+ status: result.status || "completed",
2441
+ outcome: result.outcome || "",
2442
+ duration_seconds: typeof result.durationSeconds === "number" ? result.durationSeconds : 0,
2443
+ cost_usd: costUsd,
2444
+ transcript: result.transcript ? [...result.transcript] : [],
2445
+ metrics
2446
+ };
2447
+ }
2338
2448
 
2339
2449
  // src/providers/gemini-live.ts
2340
2450
  init_esm_shims();
@@ -2905,6 +3015,12 @@ var ELEVENLABS_VOICE_ID_BY_NAME = {
2905
3015
  alloy: "EXAVITQu4vr4xnSDxMaL"
2906
3016
  };
2907
3017
  var VOICE_ID_PATTERN = /^[A-Za-z0-9]{20}$/;
3018
+ var CARRIER_NATIVE_FORMAT = {
3019
+ twilio: "ulaw_8000",
3020
+ telnyx: "pcm_16000",
3021
+ // Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
3022
+ plivo: "ulaw_8000"
3023
+ };
2908
3024
  function resolveVoiceId(voice) {
2909
3025
  if (!voice) return voice;
2910
3026
  if (VOICE_ID_PATTERN.test(voice)) return voice;
@@ -2993,11 +3109,8 @@ var ElevenLabsTTS = class _ElevenLabsTTS {
2993
3109
  */
2994
3110
  setTelephonyCarrier(carrier) {
2995
3111
  if (this._outputFormatExplicit) return;
2996
- if (carrier === "twilio") {
2997
- this._outputFormat = ElevenLabsOutputFormat.ULAW_8000;
2998
- } else if (carrier === "telnyx") {
2999
- this._outputFormat = ElevenLabsOutputFormat.PCM_16000;
3000
- }
3112
+ const native = CARRIER_NATIVE_FORMAT[carrier];
3113
+ if (native !== void 0) this._outputFormat = native;
3001
3114
  }
3002
3115
  /**
3003
3116
  * Construct an instance pre-configured for Twilio Media Streams.
@@ -5215,9 +5328,11 @@ var PLAN_REQUIRED_MSG = "ElevenLabs WS streaming requires a Pro plan or higher (
5215
5328
  function sanitiseLogStr(value, limit = 200) {
5216
5329
  return String(value).replace(/[\r\n\x00]/g, " ").slice(0, limit);
5217
5330
  }
5218
- var CARRIER_NATIVE_FORMAT = {
5331
+ var CARRIER_NATIVE_FORMAT2 = {
5219
5332
  twilio: "ulaw_8000",
5220
- telnyx: "pcm_16000"
5333
+ telnyx: "pcm_16000",
5334
+ // Plivo streams mulaw 8 kHz (we pin contentType in the answer XML).
5335
+ plivo: "ulaw_8000"
5221
5336
  };
5222
5337
  var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
5223
5338
  static providerKey = "elevenlabs_ws";
@@ -5301,7 +5416,7 @@ var ElevenLabsWebSocketTTS = class _ElevenLabsWebSocketTTS {
5301
5416
  */
5302
5417
  setTelephonyCarrier(carrier) {
5303
5418
  if (this._outputFormatExplicit) return;
5304
- const native = CARRIER_NATIVE_FORMAT[carrier];
5419
+ const native = CARRIER_NATIVE_FORMAT2[carrier];
5305
5420
  if (!native) return;
5306
5421
  this._outputFormat = native;
5307
5422
  }
@@ -7445,7 +7560,7 @@ var KrispVivaFilter = class {
7445
7560
 
7446
7561
  // src/telephony/twilio.ts
7447
7562
  init_esm_shims();
7448
- var Carrier = class {
7563
+ var Carrier2 = class {
7449
7564
  kind = "twilio";
7450
7565
  accountSid;
7451
7566
  authToken;
@@ -7469,7 +7584,7 @@ var Carrier = class {
7469
7584
 
7470
7585
  // src/telephony/telnyx.ts
7471
7586
  init_esm_shims();
7472
- var Carrier2 = class {
7587
+ var Carrier3 = class {
7473
7588
  kind = "telnyx";
7474
7589
  apiKey;
7475
7590
  connectionId;
@@ -8757,6 +8872,8 @@ export {
8757
8872
  PatterTool,
8758
8873
  PcmCarry,
8759
8874
  PipelineHookExecutor,
8875
+ Carrier as Plivo,
8876
+ PlivoAdapter,
8760
8877
  PricingUnit,
8761
8878
  ProvisionError,
8762
8879
  RateLimitError,
@@ -8783,7 +8900,7 @@ export {
8783
8900
  TurnDetectionMode as SpeechmaticsTurnDetectionMode,
8784
8901
  StatefulResampler,
8785
8902
  Static as StaticTunnel,
8786
- Carrier2 as Telnyx,
8903
+ Carrier3 as Telnyx,
8787
8904
  TelnyxAdapter,
8788
8905
  TelnyxSTT,
8789
8906
  TelnyxSTTInputFormat,
@@ -8794,7 +8911,7 @@ export {
8794
8911
  TestSession,
8795
8912
  TfidfLoopDetector,
8796
8913
  Tool,
8797
- Carrier as Twilio,
8914
+ Carrier2 as Twilio,
8798
8915
  TwilioAdapter,
8799
8916
  ULTRAVOX_DEFAULT_API_BASE,
8800
8917
  ULTRAVOX_DEFAULT_SR,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TestSession
3
- } from "./chunk-LE63CSOB.mjs";
3
+ } from "./chunk-Z6W5XFWS.mjs";
4
4
  import "./chunk-CL2U3YET.mjs";
5
5
  import "./chunk-MVOQFAEO.mjs";
6
6
  import "./chunk-N565J3CF.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getpatter",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Open-source voice AI SDK — connect any AI agent to real phone calls in 4 lines of code",
5
5
  "license": "MIT",
6
6
  "author": {