getpatter 0.5.3 → 0.6.0

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Nicolò Tognoni
3
+ Copyright (c) 2026 Patter Contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -62,6 +62,7 @@ await phone.serve({ agent, tunnel: true });
62
62
  | Call recording | `serve({ recording: true })` | Record all calls |
63
63
  | Call transfer | `transfer_call` (auto-injected) | Transfer to a human |
64
64
  | Voicemail drop | `call({ voicemailMessage: "..." })` | Play message on voicemail |
65
+ | Phone-as-a-tool (external agents) | `new PatterTool({ phone, agent }).execute(...)` | Drop into LangChain / OpenAI Assistants / Hermes / MCP |
65
66
 
66
67
  ## Configuration
67
68
 
@@ -182,7 +183,7 @@ import {
182
183
  // Engines
183
184
  OpenAIRealtime, ElevenLabsConvAI,
184
185
  // STT
185
- DeepgramSTT, WhisperSTT, CartesiaSTT, SonioxSTT, AssemblyAISTT,
186
+ DeepgramSTT, WhisperSTT, OpenAITranscribeSTT, CartesiaSTT, SonioxSTT, AssemblyAISTT,
186
187
  // TTS
187
188
  ElevenLabsTTS, OpenAITTS, CartesiaTTS, RimeTTS, LMNTTTS,
188
189
  // LLM
@@ -191,6 +192,8 @@ import {
191
192
  CloudflareTunnel, StaticTunnel,
192
193
  // Primitives
193
194
  Tool, Guardrail, tool, guardrail,
195
+ // Integrations
196
+ PatterTool,
194
197
  } from "getpatter";
195
198
  ```
196
199
 
@@ -306,7 +309,7 @@ const agent = phone.agent({
306
309
  Pull requests are welcome.
307
310
 
308
311
  ```bash
309
- cd sdk-ts && npm install && npm test
312
+ cd libraries/typescript && npm install && npm test
310
313
  ```
311
314
 
312
315
  Please open an issue before submitting large changes so we can discuss the approach first.
@@ -0,0 +1,228 @@
1
+ import {
2
+ init_esm_shims
3
+ } from "./chunk-N565J3CF.mjs";
4
+
5
+ // src/audio/aec.ts
6
+ init_esm_shims();
7
+ var DEFAULT_FILTER_TAPS = 512;
8
+ var DEFAULT_STEP_SIZE = 0.1;
9
+ var DEFAULT_WARMUP_STEP_SIZE = 0.5;
10
+ var DEFAULT_WARMUP_SECONDS = 0.5;
11
+ var DEFAULT_LEAKAGE = 0.9999;
12
+ var DEFAULT_DOUBLE_TALK_RHO = 0.6;
13
+ var FAR_END_BUFFER_SECONDS = 0.5;
14
+ var NlmsEchoCanceller = class {
15
+ taps;
16
+ step;
17
+ warmupStep;
18
+ warmupSamples;
19
+ leakage;
20
+ rho;
21
+ /** Filter coefficients — adapt to match the channel impulse response. */
22
+ w;
23
+ /** Far-end ring buffer of past TTS samples, in chronological order. */
24
+ farBuf;
25
+ farWriteIdx = 0;
26
+ farFilled = 0;
27
+ /**
28
+ * Sample counter used to taper the step from `warmupStep` down to
29
+ * `step` over the first `warmupSamples` of processed near-end audio.
30
+ * Counted from the first `processNearEnd` call (not construction
31
+ * time) so the warmup window aligns with the actual start of TTS
32
+ * playback rather than agent setup.
33
+ */
34
+ processedSamples = 0;
35
+ /** Stats — used for diagnostics, never read on the hot path. */
36
+ framesProcessed = 0;
37
+ doubleTalkFrames = 0;
38
+ constructor(opts = {}) {
39
+ const sampleRate = opts.sampleRate ?? 16e3;
40
+ if (sampleRate !== 8e3 && sampleRate !== 16e3) {
41
+ throw new Error(
42
+ `NlmsEchoCanceller supports 8000 Hz or 16000 Hz only; got ${sampleRate}.`
43
+ );
44
+ }
45
+ const taps = opts.filterTaps ?? DEFAULT_FILTER_TAPS;
46
+ if (taps < 64) {
47
+ throw new Error(
48
+ `filterTaps must be >= 64 to model a meaningful echo path; got ${taps}.`
49
+ );
50
+ }
51
+ const step = opts.stepSize ?? DEFAULT_STEP_SIZE;
52
+ if (!(step > 0 && step <= 1)) {
53
+ throw new Error(`stepSize must be in (0, 1]; got ${step}.`);
54
+ }
55
+ const warmupStep = opts.warmupStepSize ?? DEFAULT_WARMUP_STEP_SIZE;
56
+ if (!(warmupStep > 0 && warmupStep <= 1)) {
57
+ throw new Error(
58
+ `warmupStepSize must be in (0, 1]; got ${warmupStep}.`
59
+ );
60
+ }
61
+ const warmupSeconds = opts.warmupSeconds ?? DEFAULT_WARMUP_SECONDS;
62
+ if (warmupSeconds < 0) {
63
+ throw new Error(
64
+ `warmupSeconds must be >= 0; got ${warmupSeconds}.`
65
+ );
66
+ }
67
+ const leakage = opts.leakage ?? DEFAULT_LEAKAGE;
68
+ if (!(leakage > 0 && leakage <= 1)) {
69
+ throw new Error(`leakage must be in (0, 1]; got ${leakage}.`);
70
+ }
71
+ this.taps = taps;
72
+ this.step = step;
73
+ this.warmupStep = warmupStep;
74
+ this.warmupSamples = Math.floor(warmupSeconds * sampleRate);
75
+ this.leakage = leakage;
76
+ this.rho = opts.doubleTalkRho ?? DEFAULT_DOUBLE_TALK_RHO;
77
+ this.w = new Float32Array(taps);
78
+ const farBufSize = Math.max(
79
+ taps * 2,
80
+ Math.floor(sampleRate * FAR_END_BUFFER_SECONDS)
81
+ );
82
+ this.farBuf = new Float32Array(farBufSize);
83
+ }
84
+ /**
85
+ * Append far-end (TTS) audio to the reference ring buffer.
86
+ *
87
+ * Accepts raw int16 little-endian mono PCM at the configured sample
88
+ * rate — same shape as what we hand off to the audio sender before the
89
+ * carrier-specific transcode.
90
+ */
91
+ pushFarEnd(pcm) {
92
+ if (pcm.length === 0) return;
93
+ const samples = int16BufferToFloat32(pcm);
94
+ const n = samples.length;
95
+ const bufLen = this.farBuf.length;
96
+ if (n >= bufLen) {
97
+ this.farBuf.set(samples.subarray(n - bufLen));
98
+ this.farWriteIdx = 0;
99
+ this.farFilled = bufLen;
100
+ return;
101
+ }
102
+ const end = this.farWriteIdx + n;
103
+ if (end <= bufLen) {
104
+ this.farBuf.set(samples, this.farWriteIdx);
105
+ } else {
106
+ const head = bufLen - this.farWriteIdx;
107
+ this.farBuf.set(samples.subarray(0, head), this.farWriteIdx);
108
+ this.farBuf.set(samples.subarray(head), 0);
109
+ }
110
+ this.farWriteIdx = (this.farWriteIdx + n) % bufLen;
111
+ this.farFilled = Math.min(this.farFilled + n, bufLen);
112
+ }
113
+ /**
114
+ * Subtract estimated echo from the near-end (mic) signal.
115
+ *
116
+ * Returns int16 little-endian mono PCM with the estimated echo
117
+ * removed. When the far-end buffer hasn't been primed yet (no TTS has
118
+ * played) the call is a pass-through — there is nothing to cancel.
119
+ */
120
+ processNearEnd(pcm) {
121
+ if (pcm.length === 0) return pcm;
122
+ if (this.farFilled < this.taps) return pcm;
123
+ const near = int16BufferToFloat32(pcm);
124
+ const cleaned = this.blockNlms(near);
125
+ this.framesProcessed += 1;
126
+ return float32ToInt16Buffer(cleaned);
127
+ }
128
+ /**
129
+ * Clear filter coefficients and far-end history.
130
+ *
131
+ * Useful between unrelated turns when the echo path may have changed
132
+ * (e.g. caller switched from speakerphone to handset).
133
+ */
134
+ reset() {
135
+ this.w.fill(0);
136
+ this.farBuf.fill(0);
137
+ this.farWriteIdx = 0;
138
+ this.farFilled = 0;
139
+ this.processedSamples = 0;
140
+ this.framesProcessed = 0;
141
+ this.doubleTalkFrames = 0;
142
+ }
143
+ // --------------------------------------------------------------------
144
+ // Internals
145
+ // --------------------------------------------------------------------
146
+ /** Most-recent ``length`` far-end samples in chronological order. */
147
+ farWindow(length) {
148
+ const bufLen = this.farBuf.length;
149
+ const len = Math.min(length, this.farFilled);
150
+ const end = this.farWriteIdx;
151
+ if (end >= len) {
152
+ return this.farBuf.subarray(end - len, end);
153
+ }
154
+ const head = bufLen - (len - end);
155
+ const out = new Float32Array(len);
156
+ out.set(this.farBuf.subarray(head));
157
+ out.set(this.farBuf.subarray(0, end), bufLen - head);
158
+ return out;
159
+ }
160
+ /** Sample-by-sample NLMS over a frame of near-end samples. */
161
+ blockNlms(near) {
162
+ const taps = this.taps;
163
+ const desiredLen = taps + near.length - 1;
164
+ let farWindow = this.farWindow(desiredLen);
165
+ if (farWindow.length < desiredLen) {
166
+ const padded = new Float32Array(desiredLen);
167
+ padded.set(farWindow, desiredLen - farWindow.length);
168
+ farWindow = padded;
169
+ }
170
+ let farMax = 0;
171
+ for (let i = 0; i < farWindow.length; i++) {
172
+ const a = Math.abs(farWindow[i]);
173
+ if (a > farMax) farMax = a;
174
+ }
175
+ let nearMax = 0;
176
+ for (let i = 0; i < near.length; i++) {
177
+ const a = Math.abs(near[i]);
178
+ if (a > nearMax) nearMax = a;
179
+ }
180
+ const doubleTalk = farMax > 1e-6 ? nearMax > this.rho * farMax : false;
181
+ if (doubleTalk) this.doubleTalkFrames += 1;
182
+ const out = new Float32Array(near.length);
183
+ const w = this.w;
184
+ const leakage = this.leakage;
185
+ const step = this.processedSamples < this.warmupSamples ? this.warmupStep : this.step;
186
+ for (let i = 0; i < near.length; i++) {
187
+ let yEst = 0;
188
+ let norm = 0;
189
+ const base = i;
190
+ for (let k = 0; k < taps; k++) {
191
+ const xk = farWindow[base + k];
192
+ yEst += w[k] * xk;
193
+ norm += xk * xk;
194
+ }
195
+ const e = near[i] - yEst;
196
+ out[i] = e;
197
+ if (!doubleTalk) {
198
+ const denom = norm + 1e-6;
199
+ const factor = step * e / denom;
200
+ for (let k = 0; k < taps; k++) {
201
+ w[k] = leakage * w[k] + factor * farWindow[base + k];
202
+ }
203
+ }
204
+ }
205
+ this.processedSamples += near.length;
206
+ return out;
207
+ }
208
+ };
209
+ function int16BufferToFloat32(buf) {
210
+ const samples = new Float32Array(buf.length / 2);
211
+ for (let i = 0; i < samples.length; i++) {
212
+ samples[i] = buf.readInt16LE(i * 2) / 32768;
213
+ }
214
+ return samples;
215
+ }
216
+ function float32ToInt16Buffer(samples) {
217
+ const out = Buffer.alloc(samples.length * 2);
218
+ for (let i = 0; i < samples.length; i++) {
219
+ let v = Math.round(samples[i] * 32768);
220
+ if (v > 32767) v = 32767;
221
+ else if (v < -32768) v = -32768;
222
+ out.writeInt16LE(v, i * 2);
223
+ }
224
+ return out;
225
+ }
226
+ export {
227
+ NlmsEchoCanceller
228
+ };
@@ -1,6 +1,9 @@
1
- import "./chunk-QHHBUCMT.mjs";
1
+ import {
2
+ init_esm_shims
3
+ } from "./chunk-N565J3CF.mjs";
2
4
 
3
5
  // src/banner.ts
6
+ init_esm_shims();
4
7
  var BANNER = `
5
8
  \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
6
9
  \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
@@ -1,9 +1,12 @@
1
1
  import {
2
2
  getLogger
3
- } from "./chunk-VJVDG4V5.mjs";
4
- import "./chunk-QHHBUCMT.mjs";
3
+ } from "./chunk-MVOQFAEO.mjs";
4
+ import {
5
+ init_esm_shims
6
+ } from "./chunk-N565J3CF.mjs";
5
7
 
6
8
  // src/carrier-config.ts
9
+ init_esm_shims();
7
10
  var TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
8
11
  var TELNYX_API_BASE = "https://api.telnyx.com/v2";
9
12
  async function configureTwilioNumber(accountSid, authToken, phoneNumber, voiceUrl) {
@@ -1,6 +1,15 @@
1
+ import {
2
+ init_esm_shims
3
+ } from "./chunk-N565J3CF.mjs";
4
+
1
5
  // src/dashboard/persistence.ts
6
+ init_esm_shims();
2
7
  import http from "http";
3
8
  function notifyDashboard(callData, port = 8e3) {
9
+ const flag = (process.env.PATTER_DASHBOARD_NOTIFY ?? "").trim().toLowerCase();
10
+ if (flag === "0" || flag === "false" || flag === "no" || flag === "off") {
11
+ return;
12
+ }
4
13
  try {
5
14
  const body = JSON.stringify(callData);
6
15
  const req = http.request(