realtime-voice-agents 2.2.0 → 2.3.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/README.md +31 -1
- package/dist/index.cjs +158 -3
- package/dist/index.d.cts +146 -1
- package/dist/index.d.mts +146 -1
- package/dist/index.mjs +154 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -270,9 +270,38 @@ session: { greeting: { mode: 'agent-initiates',
|
|
|
270
270
|
|
|
271
271
|
Bundled presets (all synthesized, license-free, seamless loops): `elevator-jazz`, `lofi`, `keyboard-typing`, `thinking-hum`, `ringing` — or `{ custom: bufferOrPath }` with your own 8 kHz μ-law. Drift-corrected 20 ms pacing, refcounted across concurrent tools, ~1 s start delay so fast tools stay silent, fade in/out, 60 s failsafe, and instant preemption when real speech arrives. Manual control: `session.playBackgroundAudio('lofi')` / `stopBackgroundAudio()`.
|
|
272
272
|
|
|
273
|
+
## Keypad input (DTMF, opt-in)
|
|
274
|
+
|
|
275
|
+
Callers type an ID, a phone number, a confirmation code — Twilio delivers each key as its own `dtmf` frame, ~1s apart, and a model that sees ten fragments answers "I didn't get that" ten times. `keypad` turns keypresses into one entry: digits buffer, `#` submits, `*` clears, `maxDigits` auto-submits, 4s without a key flushes what's there (so the agent can say "that's only 7 digits — again, please"). Each keypress stops the agent mid-sentence (typing means "I'm answering"), and the entry reaches the model as a **user** turn — `[keypad] I typed on my phone keypad: 0541234567 — 10 digits. Digit by digit: 0 5 4 …` — that triggers the response answering it. A short note appended to the agent instructions tells the model what `[keypad]` messages are.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
session: {
|
|
279
|
+
keypad: {}, // {} = defaults below
|
|
280
|
+
// maxDigits: 9, // auto-submit at N digits (no # needed)
|
|
281
|
+
// interDigitTimeoutMs: 4000, submitKey: '#', clearKey: '*',
|
|
282
|
+
// interruptOnKeypress: true, // false: the agent keeps talking while the caller types
|
|
283
|
+
// message: (entry) => string | false, // wording of the injected user turn; false = events only
|
|
284
|
+
// clearMessage: string | false, // what the model hears on *; false = nothing
|
|
285
|
+
// instructions: string | false, // the appended note; false = your prompt says it
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Observe or take over with events and the `session.keypad` handle (`digits`, `clear()`, `submit()`):
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
session.on('keypad.entry', ({ digits, reason }) => { /* reason: 'submit' | 'timeout' | 'maxDigits' */ });
|
|
293
|
+
session.on('keypad.cleared', ({ discarded }) => { /* caller pressed * */ });
|
|
294
|
+
// Raw keypresses still fire per key — AFTER the collector consumed them, so the handle is current:
|
|
295
|
+
session.on('dtmf', ({ digit }) => {
|
|
296
|
+
if (digit === '0' && session.keypad.digits === '0') { session.keypad.clear(); void session.transferTo(OPERATOR); }
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`message: false` keeps the collection and events but injects nothing — validate the entry yourself and `session.sendText(..., { role: 'user', triggerResponse: true })` what the model should hear. Role `user`, not `system`: a trailing system item is skipped by the response it triggers and only lands one response later (field-tested on xAI). Without `keypad` configured nothing changes: raw `dtmf` events only, as before. Letters A–D are ignored; the buffer dies with the call.
|
|
301
|
+
|
|
273
302
|
## Events (session)
|
|
274
303
|
|
|
275
|
-
`call.started/ended/failed` · `provider.connected/fallback/reconnecting/reconnected/closed` · `agent.speech.started/ended` (generation) · **`playback.started/finished/interrupted`** (what the caller heard, mark-confirmed) · `user.speech.started/ended` · `transcript.user/agent` · `tool.started/completed/failed` · `tool.approval.required` · `agent.handoff` · `interruption` / `interruption.blocked` · `vad.suggestion` / `vad.adjusted` (noise-adaptive VAD) · `background_audio.started/stopped` · `dtmf` · `usage.updated` · `error`.
|
|
304
|
+
`call.started/ended/failed` · `provider.connected/fallback/reconnecting/reconnected/closed` · `agent.speech.started/ended` (generation) · **`playback.started/finished/interrupted`** (what the caller heard, mark-confirmed) · `user.speech.started/ended` · `transcript.user/agent` · `tool.started/completed/failed` · `tool.approval.required` · `agent.handoff` · `interruption` / `interruption.blocked` · `vad.suggestion` / `vad.adjusted` (noise-adaptive VAD) · `background_audio.started/stopped` · `dtmf` (raw keypress) · `keypad.entry` / `keypad.cleared` (keypad input) · `usage.updated` · `error`.
|
|
276
305
|
|
|
277
306
|
```ts
|
|
278
307
|
bridge.on('session.started', (session) => {
|
|
@@ -300,6 +329,7 @@ session: {
|
|
|
300
329
|
hangup: { markTimeoutMs: 7000 }, // goodbye watchdog
|
|
301
330
|
vad: undefined, // normalized VAD, mapped per provider
|
|
302
331
|
noiseAdaptiveVad: undefined, // opt-in noise → VAD escalation ({} enables; see its section)
|
|
332
|
+
keypad: undefined, // opt-in DTMF → one user turn per entry ({} enables; see its section)
|
|
303
333
|
toolResultDelivery: 'afterPlayback', // or 'immediate'
|
|
304
334
|
toolBackgroundAudio: undefined, // default hold audio for tools
|
|
305
335
|
handoffVoicePolicy: 'keep', // or 'reconnect' to switch voices
|
package/dist/index.cjs
CHANGED
|
@@ -193,6 +193,105 @@ function createHandoffTool(target) {
|
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
195
|
//#endregion
|
|
196
|
+
//#region src/dtmf/KeypadCollector.ts
|
|
197
|
+
const DEFAULT_KEYPAD_OPTIONS = {
|
|
198
|
+
submitKey: "#",
|
|
199
|
+
clearKey: "*",
|
|
200
|
+
interDigitTimeoutMs: 4e3,
|
|
201
|
+
interruptOnKeypress: true
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Appended to the agent instructions when keypad input is enabled. Short and
|
|
205
|
+
* neutral on purpose: what the messages are, not how to run the dialog.
|
|
206
|
+
*/
|
|
207
|
+
const DEFAULT_KEYPAD_INSTRUCTIONS = "Keypad input: the caller may type on their phone keypad instead of speaking. Keypresses arrive as a user message starting with \"[keypad]\" that contains the typed digits — treat it as the caller's answer.";
|
|
208
|
+
/** Default text of the user turn injected for a completed entry. */
|
|
209
|
+
function defaultKeypadMessage(entry) {
|
|
210
|
+
const { digits } = entry;
|
|
211
|
+
return `[keypad] I typed on my phone keypad: ${digits} — ${digits.length} digit${digits.length === 1 ? "" : "s"}. Digit by digit: ${[...digits].join(" ")}`;
|
|
212
|
+
}
|
|
213
|
+
/** Default text of the user turn injected when the caller presses the clear key. */
|
|
214
|
+
const DEFAULT_KEYPAD_CLEAR_MESSAGE = "[keypad] I pressed star — I want to start over and retype from the beginning.";
|
|
215
|
+
var KeypadCollector = class {
|
|
216
|
+
submitKey;
|
|
217
|
+
clearKey;
|
|
218
|
+
interDigitTimeoutMs;
|
|
219
|
+
maxDigits;
|
|
220
|
+
hooks;
|
|
221
|
+
buffer = "";
|
|
222
|
+
timer = null;
|
|
223
|
+
disposed = false;
|
|
224
|
+
constructor(options, hooks) {
|
|
225
|
+
this.submitKey = options.submitKey ?? DEFAULT_KEYPAD_OPTIONS.submitKey;
|
|
226
|
+
this.clearKey = options.clearKey ?? DEFAULT_KEYPAD_OPTIONS.clearKey;
|
|
227
|
+
this.interDigitTimeoutMs = options.interDigitTimeoutMs ?? DEFAULT_KEYPAD_OPTIONS.interDigitTimeoutMs;
|
|
228
|
+
this.maxDigits = options.maxDigits !== void 0 && options.maxDigits > 0 ? options.maxDigits : void 0;
|
|
229
|
+
this.hooks = hooks;
|
|
230
|
+
}
|
|
231
|
+
get digits() {
|
|
232
|
+
return this.buffer;
|
|
233
|
+
}
|
|
234
|
+
/** Feed one keypress (a Twilio `dtmf` frame). Returns what the key meant. */
|
|
235
|
+
press(key) {
|
|
236
|
+
if (this.disposed) return "ignored";
|
|
237
|
+
if (key === this.submitKey) {
|
|
238
|
+
this.cancelTimer();
|
|
239
|
+
this.complete("submit");
|
|
240
|
+
return "submit";
|
|
241
|
+
}
|
|
242
|
+
if (key === this.clearKey) {
|
|
243
|
+
this.cancelTimer();
|
|
244
|
+
const discarded = this.buffer;
|
|
245
|
+
this.buffer = "";
|
|
246
|
+
this.hooks.onClear({ discarded });
|
|
247
|
+
return "clear";
|
|
248
|
+
}
|
|
249
|
+
if (!/^[0-9]$/.test(key)) return "ignored";
|
|
250
|
+
this.cancelTimer();
|
|
251
|
+
this.buffer += key;
|
|
252
|
+
if (this.maxDigits !== void 0 && this.buffer.length >= this.maxDigits) this.complete("maxDigits");
|
|
253
|
+
else this.armTimer();
|
|
254
|
+
return "digit";
|
|
255
|
+
}
|
|
256
|
+
clear() {
|
|
257
|
+
this.cancelTimer();
|
|
258
|
+
this.buffer = "";
|
|
259
|
+
}
|
|
260
|
+
submit() {
|
|
261
|
+
if (this.disposed) return;
|
|
262
|
+
this.cancelTimer();
|
|
263
|
+
this.complete("submit");
|
|
264
|
+
}
|
|
265
|
+
/** Release the timer; pending digits are dropped (the call is over). */
|
|
266
|
+
dispose() {
|
|
267
|
+
this.disposed = true;
|
|
268
|
+
this.cancelTimer();
|
|
269
|
+
this.buffer = "";
|
|
270
|
+
}
|
|
271
|
+
complete(reason) {
|
|
272
|
+
if (!this.buffer) return;
|
|
273
|
+
const digits = this.buffer;
|
|
274
|
+
this.buffer = "";
|
|
275
|
+
this.hooks.onEntry({
|
|
276
|
+
digits,
|
|
277
|
+
reason
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
armTimer() {
|
|
281
|
+
this.timer = setTimeout(() => {
|
|
282
|
+
this.timer = null;
|
|
283
|
+
this.complete("timeout");
|
|
284
|
+
}, this.interDigitTimeoutMs);
|
|
285
|
+
this.timer.unref?.();
|
|
286
|
+
}
|
|
287
|
+
cancelTimer() {
|
|
288
|
+
if (this.timer) {
|
|
289
|
+
clearTimeout(this.timer);
|
|
290
|
+
this.timer = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
//#endregion
|
|
196
295
|
//#region src/interruption/InterruptionController.ts
|
|
197
296
|
var InterruptionController = class {
|
|
198
297
|
settings;
|
|
@@ -893,11 +992,17 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
893
992
|
streamSid;
|
|
894
993
|
callInfo;
|
|
895
994
|
context;
|
|
995
|
+
/**
|
|
996
|
+
* Keypad (DTMF) input handle: digits buffered so far, `clear()`, `submit()`.
|
|
997
|
+
* Inert (empty, no-ops) unless the `keypad` session option is configured.
|
|
998
|
+
*/
|
|
999
|
+
keypad;
|
|
896
1000
|
stateValue = "connecting";
|
|
897
1001
|
deps;
|
|
898
1002
|
log;
|
|
899
1003
|
tracker = new PlaybackTracker();
|
|
900
1004
|
interruptions;
|
|
1005
|
+
keypadCollector;
|
|
901
1006
|
usageAccumulator = new UsageAccumulator();
|
|
902
1007
|
toolQueue = new ToolResultQueue();
|
|
903
1008
|
transcriptEntries = [];
|
|
@@ -971,6 +1076,18 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
971
1076
|
this.activeAgentValue = deps.agent;
|
|
972
1077
|
this.context = new SessionContext(deps.options.context);
|
|
973
1078
|
this.interruptions = new InterruptionController(deps.options.interruptions);
|
|
1079
|
+
this.keypadCollector = deps.options.keypad ? new KeypadCollector(deps.options.keypad, {
|
|
1080
|
+
onEntry: (entry) => this.onKeypadEntry(entry),
|
|
1081
|
+
onClear: (info) => this.onKeypadCleared(info)
|
|
1082
|
+
}) : null;
|
|
1083
|
+
const collector = this.keypadCollector;
|
|
1084
|
+
this.keypad = {
|
|
1085
|
+
get digits() {
|
|
1086
|
+
return collector?.digits ?? "";
|
|
1087
|
+
},
|
|
1088
|
+
clear: () => collector?.clear(),
|
|
1089
|
+
submit: () => collector?.submit()
|
|
1090
|
+
};
|
|
974
1091
|
const params = deps.start.start.customParameters ?? {};
|
|
975
1092
|
this.callInfo = {
|
|
976
1093
|
direction: params.direction === "outbound" ? "outbound" : "inbound",
|
|
@@ -1212,9 +1329,16 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
1212
1329
|
}
|
|
1213
1330
|
return tools;
|
|
1214
1331
|
}
|
|
1332
|
+
/** Agent instructions plus the kit's own notes that must survive handoffs (keypad). */
|
|
1333
|
+
composeInstructions() {
|
|
1334
|
+
const base = this.activeAgentValue.resolveInstructions(this.context);
|
|
1335
|
+
const keypad = this.deps.options.keypad;
|
|
1336
|
+
if (!keypad || keypad.instructions === false) return base;
|
|
1337
|
+
return `${base}\n\n${keypad.instructions ?? "Keypad input: the caller may type on their phone keypad instead of speaking. Keypresses arrive as a user message starting with \"[keypad]\" that contains the typed digits — treat it as the caller's answer."}`;
|
|
1338
|
+
}
|
|
1215
1339
|
buildProviderInit() {
|
|
1216
1340
|
return {
|
|
1217
|
-
instructions: this.
|
|
1341
|
+
instructions: this.composeInstructions() + (this.pregreeting ? `\n\nYou already opened the call by saying: "${this.pregreeting.text}". Do not greet again — continue the conversation from there.` : ""),
|
|
1218
1342
|
voice: this.activeAgentValue.voice,
|
|
1219
1343
|
vad: this.vadOverride !== void 0 ? this.vadOverride : this.deps.options.vad,
|
|
1220
1344
|
bridgeOwnsInterruptions: true,
|
|
@@ -1237,7 +1361,9 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
1237
1361
|
transport.on("dtmf", (event) => {
|
|
1238
1362
|
this.clearIdleTimer();
|
|
1239
1363
|
this.nudgeCount = 0;
|
|
1240
|
-
|
|
1364
|
+
const digit = event.dtmf.digit;
|
|
1365
|
+
this.handleKeypress(digit);
|
|
1366
|
+
this.emit("dtmf", { digit });
|
|
1241
1367
|
});
|
|
1242
1368
|
transport.on("stop", () => void this.teardown("caller-hangup"));
|
|
1243
1369
|
transport.on("close", () => void this.teardown("caller-hangup"));
|
|
@@ -2011,7 +2137,7 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
2011
2137
|
if (!(!this.provider.capabilities.sessionUpdate || voiceChanges && !this.provider.capabilities.voiceChangeMidSession && this.deps.options.handoffVoicePolicy === "reconnect")) {
|
|
2012
2138
|
if (voiceChanges && !this.provider.capabilities.voiceChangeMidSession) this.log.warn(`agent "${target.id}" declares voice "${target.voice}" but the provider cannot change voice mid-session — keeping the current voice (set handoffVoicePolicy: 'reconnect' to switch)`);
|
|
2013
2139
|
await this.provider.updateSession({
|
|
2014
|
-
instructions: this.
|
|
2140
|
+
instructions: this.composeInstructions(),
|
|
2015
2141
|
tools: this.buildProviderInit().tools,
|
|
2016
2142
|
providerOptions: target.providerOptions
|
|
2017
2143
|
});
|
|
@@ -2087,6 +2213,29 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
2087
2213
|
this.transcriptEntries.push(entry);
|
|
2088
2214
|
this.emit("transcript.agent", entry);
|
|
2089
2215
|
}
|
|
2216
|
+
handleKeypress(key) {
|
|
2217
|
+
if (!this.keypadCollector) return;
|
|
2218
|
+
if (this.deps.options.keypad?.interruptOnKeypress !== false) this.interrupt();
|
|
2219
|
+
this.keypadCollector.press(key);
|
|
2220
|
+
}
|
|
2221
|
+
onKeypadEntry(entry) {
|
|
2222
|
+
this.emit("keypad.entry", entry);
|
|
2223
|
+
const message = this.deps.options.keypad?.message;
|
|
2224
|
+
if (message === false) return;
|
|
2225
|
+
this.provider?.sendText((message ?? defaultKeypadMessage)(entry), {
|
|
2226
|
+
role: "user",
|
|
2227
|
+
triggerResponse: true
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
onKeypadCleared(info) {
|
|
2231
|
+
this.emit("keypad.cleared", info);
|
|
2232
|
+
const clearMessage = this.deps.options.keypad?.clearMessage;
|
|
2233
|
+
if (clearMessage === false) return;
|
|
2234
|
+
this.provider?.sendText(clearMessage ?? "[keypad] I pressed star — I want to start over and retype from the beginning.", {
|
|
2235
|
+
role: "user",
|
|
2236
|
+
triggerResponse: true
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2090
2239
|
/** Armed whenever the agent goes quiet and we're waiting on the caller. */
|
|
2091
2240
|
armIdleTimer() {
|
|
2092
2241
|
const idle = this.deps.options.idle;
|
|
@@ -2146,6 +2295,7 @@ var CallSession = class extends require_events.TypedEmitter {
|
|
|
2146
2295
|
for (const timer of this.timers) clearTimeout(timer);
|
|
2147
2296
|
this.timers.clear();
|
|
2148
2297
|
this.clearIdleTimer();
|
|
2298
|
+
this.keypadCollector?.dispose();
|
|
2149
2299
|
this.bgAudio.stop({ immediate: true });
|
|
2150
2300
|
for (const controller of this.runningTools.values()) controller.abort(/* @__PURE__ */ new Error("call ended"));
|
|
2151
2301
|
this.runningTools.clear();
|
|
@@ -2456,10 +2606,14 @@ async function captureGreetingAudio(options) {
|
|
|
2456
2606
|
exports.Agent = Agent;
|
|
2457
2607
|
exports.BaseRealtimeProvider = require_BaseRealtimeProvider.BaseRealtimeProvider;
|
|
2458
2608
|
exports.CallSession = CallSession;
|
|
2609
|
+
exports.DEFAULT_KEYPAD_CLEAR_MESSAGE = DEFAULT_KEYPAD_CLEAR_MESSAGE;
|
|
2610
|
+
exports.DEFAULT_KEYPAD_INSTRUCTIONS = DEFAULT_KEYPAD_INSTRUCTIONS;
|
|
2611
|
+
exports.DEFAULT_KEYPAD_OPTIONS = DEFAULT_KEYPAD_OPTIONS;
|
|
2459
2612
|
exports.DEFAULT_RECONNECT_POLICY = DEFAULT_RECONNECT_POLICY;
|
|
2460
2613
|
exports.DEFAULT_SESSION_OPTIONS = DEFAULT_SESSION_OPTIONS;
|
|
2461
2614
|
exports.InMemorySessionStore = require_InMemorySessionStore.InMemorySessionStore;
|
|
2462
2615
|
exports.InterruptionController = InterruptionController;
|
|
2616
|
+
exports.KeypadCollector = KeypadCollector;
|
|
2463
2617
|
exports.NoiseAdaptiveVadController = NoiseAdaptiveVadController;
|
|
2464
2618
|
exports.PlaybackTracker = PlaybackTracker;
|
|
2465
2619
|
exports.SessionContext = SessionContext;
|
|
@@ -2473,6 +2627,7 @@ exports.createFinishCallTool = createFinishCallTool;
|
|
|
2473
2627
|
exports.createHandoffTool = createHandoffTool;
|
|
2474
2628
|
exports.createTransferCallTool = createTransferCallTool;
|
|
2475
2629
|
exports.decorateTool = decorateTool;
|
|
2630
|
+
exports.defaultKeypadMessage = defaultKeypadMessage;
|
|
2476
2631
|
exports.emptyUsage = emptyUsage;
|
|
2477
2632
|
exports.handoffToolName = handoffToolName;
|
|
2478
2633
|
exports.isHandoffDirective = isHandoffDirective;
|
package/dist/index.d.cts
CHANGED
|
@@ -193,6 +193,124 @@ declare function decorateTool(tool: Tool, middlewares: readonly ToolMiddleware[]
|
|
|
193
193
|
/** Compose wrapExecute chains around an innermost executor. */
|
|
194
194
|
declare function composeExecution(tool: Tool, input: unknown, ctx: ToolContext, middlewares: readonly ToolMiddleware[], innermost: () => Promise<unknown>): Promise<unknown>;
|
|
195
195
|
//#endregion
|
|
196
|
+
//#region src/dtmf/KeypadCollector.d.ts
|
|
197
|
+
/**
|
|
198
|
+
* KeypadCollector — turns single DTMF keypresses into complete keypad entries.
|
|
199
|
+
*
|
|
200
|
+
* Callers type digits with ~1s pauses between them; the model should hear the
|
|
201
|
+
* whole number once, not ten fragments. The collector buffers digits and
|
|
202
|
+
* completes an entry on the submit key (`#`), on `maxDigits`, or after
|
|
203
|
+
* `interDigitTimeoutMs` of no keypresses (the caller stopped without `#` —
|
|
204
|
+
* flushing lets the agent react, typically "that's only N digits, please
|
|
205
|
+
* retype"). The clear key (`*`) discards the buffer so the caller can start
|
|
206
|
+
* over. Letters A–D and anything exotic are ignored.
|
|
207
|
+
*
|
|
208
|
+
* Pure state machine: no session, no transport. `CallSession` feeds it from
|
|
209
|
+
* the Twilio `dtmf` frame (before emitting the raw `dtmf` event), turns the
|
|
210
|
+
* hooks into `keypad.entry` / `keypad.cleared` events, and injects the default
|
|
211
|
+
* user-turn message. Field-tested shape (workshop DTMF agents, Aug 2026).
|
|
212
|
+
*/
|
|
213
|
+
type KeypadEntryReason = 'submit' | 'timeout' | 'maxDigits';
|
|
214
|
+
interface KeypadEntry {
|
|
215
|
+
/** The digits typed, in order (0–9 only). */
|
|
216
|
+
digits: string;
|
|
217
|
+
/** What completed the entry: the submit key, the inter-digit timeout, or `maxDigits`. */
|
|
218
|
+
reason: KeypadEntryReason;
|
|
219
|
+
}
|
|
220
|
+
interface KeypadOptions {
|
|
221
|
+
/** Key that submits the current entry immediately. Default `'#'`. */
|
|
222
|
+
submitKey?: string;
|
|
223
|
+
/** Key that discards the current entry so the caller can start over. Default `'*'`. */
|
|
224
|
+
clearKey?: string;
|
|
225
|
+
/**
|
|
226
|
+
* Silence after the last keypress that completes the entry anyway. Callers
|
|
227
|
+
* pause ~1s between digits; default 4000.
|
|
228
|
+
*/
|
|
229
|
+
interDigitTimeoutMs?: number;
|
|
230
|
+
/**
|
|
231
|
+
* Auto-submit once this many digits are buffered (an ID or phone number of
|
|
232
|
+
* known length — the caller never has to press `#`). Default: unlimited.
|
|
233
|
+
*/
|
|
234
|
+
maxDigits?: number;
|
|
235
|
+
/**
|
|
236
|
+
* Stop the agent mid-sentence on every keypress — typing means "I'm
|
|
237
|
+
* answering". Bypasses the interruption guard like `session.interrupt()`.
|
|
238
|
+
* Without it a flushed entry queues its readback behind stale speech.
|
|
239
|
+
* Default true.
|
|
240
|
+
*/
|
|
241
|
+
interruptOnKeypress?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* How a completed entry reaches the model: a function returning the text of
|
|
244
|
+
* the user turn injected for it (default `defaultKeypadMessage`), or `false`
|
|
245
|
+
* to inject nothing and handle `keypad.entry` yourself.
|
|
246
|
+
*/
|
|
247
|
+
message?: ((entry: KeypadEntry) => string) | false;
|
|
248
|
+
/**
|
|
249
|
+
* User turn injected when the caller presses the clear key (default text
|
|
250
|
+
* tells the model the caller is starting over), or `false` for none.
|
|
251
|
+
*/
|
|
252
|
+
clearMessage?: string | false;
|
|
253
|
+
/**
|
|
254
|
+
* Appended to the agent instructions so the model knows what `[keypad]`
|
|
255
|
+
* messages are (default `DEFAULT_KEYPAD_INSTRUCTIONS`), or `false` to leave
|
|
256
|
+
* the instructions untouched — say it in your own prompt instead.
|
|
257
|
+
*/
|
|
258
|
+
instructions?: string | false;
|
|
259
|
+
}
|
|
260
|
+
/** Host-facing handle: `session.keypad`. */
|
|
261
|
+
interface KeypadHandle {
|
|
262
|
+
/** Digits buffered so far (empty when keypad input is not configured). */
|
|
263
|
+
readonly digits: string;
|
|
264
|
+
/** Discard the buffer silently — no event, no message to the model. */
|
|
265
|
+
clear(): void;
|
|
266
|
+
/** Complete the buffered entry now (reason `'submit'`); no-op when empty. */
|
|
267
|
+
submit(): void;
|
|
268
|
+
}
|
|
269
|
+
declare const DEFAULT_KEYPAD_OPTIONS: {
|
|
270
|
+
readonly submitKey: "#";
|
|
271
|
+
readonly clearKey: "*";
|
|
272
|
+
readonly interDigitTimeoutMs: 4000;
|
|
273
|
+
readonly interruptOnKeypress: true;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Appended to the agent instructions when keypad input is enabled. Short and
|
|
277
|
+
* neutral on purpose: what the messages are, not how to run the dialog.
|
|
278
|
+
*/
|
|
279
|
+
declare const DEFAULT_KEYPAD_INSTRUCTIONS: string;
|
|
280
|
+
/** Default text of the user turn injected for a completed entry. */
|
|
281
|
+
declare function defaultKeypadMessage(entry: KeypadEntry): string;
|
|
282
|
+
/** Default text of the user turn injected when the caller presses the clear key. */
|
|
283
|
+
declare const DEFAULT_KEYPAD_CLEAR_MESSAGE = "[keypad] I pressed star — I want to start over and retype from the beginning.";
|
|
284
|
+
/** What a keypress meant to the collector. */
|
|
285
|
+
type KeypadKeyKind = 'digit' | 'submit' | 'clear' | 'ignored';
|
|
286
|
+
interface KeypadCollectorHooks {
|
|
287
|
+
onEntry: (entry: KeypadEntry) => void;
|
|
288
|
+
onClear: (info: {
|
|
289
|
+
discarded: string;
|
|
290
|
+
}) => void;
|
|
291
|
+
}
|
|
292
|
+
declare class KeypadCollector implements KeypadHandle {
|
|
293
|
+
private readonly submitKey;
|
|
294
|
+
private readonly clearKey;
|
|
295
|
+
private readonly interDigitTimeoutMs;
|
|
296
|
+
private readonly maxDigits;
|
|
297
|
+
private readonly hooks;
|
|
298
|
+
private buffer;
|
|
299
|
+
private timer;
|
|
300
|
+
private disposed;
|
|
301
|
+
constructor(options: KeypadOptions, hooks: KeypadCollectorHooks);
|
|
302
|
+
get digits(): string;
|
|
303
|
+
/** Feed one keypress (a Twilio `dtmf` frame). Returns what the key meant. */
|
|
304
|
+
press(key: string): KeypadKeyKind;
|
|
305
|
+
clear(): void;
|
|
306
|
+
submit(): void;
|
|
307
|
+
/** Release the timer; pending digits are dropped (the call is over). */
|
|
308
|
+
dispose(): void;
|
|
309
|
+
private complete;
|
|
310
|
+
private armTimer;
|
|
311
|
+
private cancelTimer;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
196
314
|
//#region src/interruption/InterruptionController.d.ts
|
|
197
315
|
/**
|
|
198
316
|
* InterruptionController — decides whether a barge-in is honored.
|
|
@@ -462,6 +580,15 @@ interface SessionOptions {
|
|
|
462
580
|
* this also serializes ALL mid-call session updates for the session.
|
|
463
581
|
*/
|
|
464
582
|
noiseAdaptiveVad?: NoiseAdaptiveVadOptions;
|
|
583
|
+
/**
|
|
584
|
+
* Opt-in keypad (DTMF) input: buffer keypresses into complete entries —
|
|
585
|
+
* `#` submits, `*` clears, `maxDigits` auto-submits, 4s of no keypresses
|
|
586
|
+
* flushes — stop the agent on every keypress, and inject each entry as a
|
|
587
|
+
* `[keypad] ...` user turn the model answers. Emits `keypad.entry` /
|
|
588
|
+
* `keypad.cleared`; the raw `dtmf` event keeps firing per key. Set `{}` to
|
|
589
|
+
* enable with defaults; unset = raw `dtmf` events only, as before.
|
|
590
|
+
*/
|
|
591
|
+
keypad?: KeypadOptions;
|
|
465
592
|
/**
|
|
466
593
|
* `afterPlayback` (default): tool results wait until current agent audio
|
|
467
594
|
* finishes playing. `immediate`: send as soon as the tool completes.
|
|
@@ -688,9 +815,16 @@ interface SessionEventMap {
|
|
|
688
815
|
'background_audio.stopped': (info: {
|
|
689
816
|
preset?: string;
|
|
690
817
|
}) => void;
|
|
818
|
+
/** Raw keypress (every Twilio dtmf frame). With `keypad` configured it fires AFTER the collector consumed the key. */
|
|
691
819
|
dtmf: (info: {
|
|
692
820
|
digit: string;
|
|
693
821
|
}) => void;
|
|
822
|
+
/** A complete keypad entry (`keypad` option): submit key, `maxDigits`, or inter-digit timeout. */
|
|
823
|
+
'keypad.entry': (entry: KeypadEntry) => void;
|
|
824
|
+
/** The caller pressed the clear key; `discarded` is what the buffer held. */
|
|
825
|
+
'keypad.cleared': (info: {
|
|
826
|
+
discarded: string;
|
|
827
|
+
}) => void;
|
|
694
828
|
'usage.updated': (usage: UsageInfo, delta: ProviderUsage) => void;
|
|
695
829
|
error: (error: Error) => void;
|
|
696
830
|
}
|
|
@@ -729,11 +863,17 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
729
863
|
readonly streamSid: string;
|
|
730
864
|
readonly callInfo: ToolCallInfo;
|
|
731
865
|
readonly context: SessionContext;
|
|
866
|
+
/**
|
|
867
|
+
* Keypad (DTMF) input handle: digits buffered so far, `clear()`, `submit()`.
|
|
868
|
+
* Inert (empty, no-ops) unless the `keypad` session option is configured.
|
|
869
|
+
*/
|
|
870
|
+
readonly keypad: KeypadHandle;
|
|
732
871
|
private stateValue;
|
|
733
872
|
private readonly deps;
|
|
734
873
|
private readonly log;
|
|
735
874
|
private readonly tracker;
|
|
736
875
|
private readonly interruptions;
|
|
876
|
+
private readonly keypadCollector;
|
|
737
877
|
private readonly usageAccumulator;
|
|
738
878
|
private readonly toolQueue;
|
|
739
879
|
private readonly transcriptEntries;
|
|
@@ -863,6 +1003,8 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
863
1003
|
/** Immediate teardown (no goodbye). */
|
|
864
1004
|
end(reason?: CallEndReason): Promise<void>;
|
|
865
1005
|
private buildToolset;
|
|
1006
|
+
/** Agent instructions plus the kit's own notes that must survive handoffs (keypad). */
|
|
1007
|
+
private composeInstructions;
|
|
866
1008
|
private buildProviderInit;
|
|
867
1009
|
private wireTransport;
|
|
868
1010
|
/** Emit 'error'; when the host attached no listener, log instead of losing it. */
|
|
@@ -934,6 +1076,9 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
934
1076
|
* starts immediately while the provider session is still being built.
|
|
935
1077
|
*/
|
|
936
1078
|
private playPreGreeting;
|
|
1079
|
+
private handleKeypress;
|
|
1080
|
+
private onKeypadEntry;
|
|
1081
|
+
private onKeypadCleared;
|
|
937
1082
|
/** Armed whenever the agent goes quiet and we're waiting on the caller. */
|
|
938
1083
|
private armIdleTimer;
|
|
939
1084
|
private clearIdleTimer;
|
|
@@ -1126,4 +1271,4 @@ declare class PlaybackTracker {
|
|
|
1126
1271
|
private maybeForget;
|
|
1127
1272
|
}
|
|
1128
1273
|
//#endregion
|
|
1129
|
-
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderFallbackInfo, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
|
1274
|
+
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_KEYPAD_CLEAR_MESSAGE, DEFAULT_KEYPAD_INSTRUCTIONS, DEFAULT_KEYPAD_OPTIONS, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, KeypadCollector, type KeypadCollectorHooks, type KeypadEntry, type KeypadEntryReason, type KeypadHandle, type KeypadKeyKind, type KeypadOptions, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderFallbackInfo, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, defaultKeypadMessage, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
package/dist/index.d.mts
CHANGED
|
@@ -193,6 +193,124 @@ declare function decorateTool(tool: Tool, middlewares: readonly ToolMiddleware[]
|
|
|
193
193
|
/** Compose wrapExecute chains around an innermost executor. */
|
|
194
194
|
declare function composeExecution(tool: Tool, input: unknown, ctx: ToolContext, middlewares: readonly ToolMiddleware[], innermost: () => Promise<unknown>): Promise<unknown>;
|
|
195
195
|
//#endregion
|
|
196
|
+
//#region src/dtmf/KeypadCollector.d.ts
|
|
197
|
+
/**
|
|
198
|
+
* KeypadCollector — turns single DTMF keypresses into complete keypad entries.
|
|
199
|
+
*
|
|
200
|
+
* Callers type digits with ~1s pauses between them; the model should hear the
|
|
201
|
+
* whole number once, not ten fragments. The collector buffers digits and
|
|
202
|
+
* completes an entry on the submit key (`#`), on `maxDigits`, or after
|
|
203
|
+
* `interDigitTimeoutMs` of no keypresses (the caller stopped without `#` —
|
|
204
|
+
* flushing lets the agent react, typically "that's only N digits, please
|
|
205
|
+
* retype"). The clear key (`*`) discards the buffer so the caller can start
|
|
206
|
+
* over. Letters A–D and anything exotic are ignored.
|
|
207
|
+
*
|
|
208
|
+
* Pure state machine: no session, no transport. `CallSession` feeds it from
|
|
209
|
+
* the Twilio `dtmf` frame (before emitting the raw `dtmf` event), turns the
|
|
210
|
+
* hooks into `keypad.entry` / `keypad.cleared` events, and injects the default
|
|
211
|
+
* user-turn message. Field-tested shape (workshop DTMF agents, Aug 2026).
|
|
212
|
+
*/
|
|
213
|
+
type KeypadEntryReason = 'submit' | 'timeout' | 'maxDigits';
|
|
214
|
+
interface KeypadEntry {
|
|
215
|
+
/** The digits typed, in order (0–9 only). */
|
|
216
|
+
digits: string;
|
|
217
|
+
/** What completed the entry: the submit key, the inter-digit timeout, or `maxDigits`. */
|
|
218
|
+
reason: KeypadEntryReason;
|
|
219
|
+
}
|
|
220
|
+
interface KeypadOptions {
|
|
221
|
+
/** Key that submits the current entry immediately. Default `'#'`. */
|
|
222
|
+
submitKey?: string;
|
|
223
|
+
/** Key that discards the current entry so the caller can start over. Default `'*'`. */
|
|
224
|
+
clearKey?: string;
|
|
225
|
+
/**
|
|
226
|
+
* Silence after the last keypress that completes the entry anyway. Callers
|
|
227
|
+
* pause ~1s between digits; default 4000.
|
|
228
|
+
*/
|
|
229
|
+
interDigitTimeoutMs?: number;
|
|
230
|
+
/**
|
|
231
|
+
* Auto-submit once this many digits are buffered (an ID or phone number of
|
|
232
|
+
* known length — the caller never has to press `#`). Default: unlimited.
|
|
233
|
+
*/
|
|
234
|
+
maxDigits?: number;
|
|
235
|
+
/**
|
|
236
|
+
* Stop the agent mid-sentence on every keypress — typing means "I'm
|
|
237
|
+
* answering". Bypasses the interruption guard like `session.interrupt()`.
|
|
238
|
+
* Without it a flushed entry queues its readback behind stale speech.
|
|
239
|
+
* Default true.
|
|
240
|
+
*/
|
|
241
|
+
interruptOnKeypress?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* How a completed entry reaches the model: a function returning the text of
|
|
244
|
+
* the user turn injected for it (default `defaultKeypadMessage`), or `false`
|
|
245
|
+
* to inject nothing and handle `keypad.entry` yourself.
|
|
246
|
+
*/
|
|
247
|
+
message?: ((entry: KeypadEntry) => string) | false;
|
|
248
|
+
/**
|
|
249
|
+
* User turn injected when the caller presses the clear key (default text
|
|
250
|
+
* tells the model the caller is starting over), or `false` for none.
|
|
251
|
+
*/
|
|
252
|
+
clearMessage?: string | false;
|
|
253
|
+
/**
|
|
254
|
+
* Appended to the agent instructions so the model knows what `[keypad]`
|
|
255
|
+
* messages are (default `DEFAULT_KEYPAD_INSTRUCTIONS`), or `false` to leave
|
|
256
|
+
* the instructions untouched — say it in your own prompt instead.
|
|
257
|
+
*/
|
|
258
|
+
instructions?: string | false;
|
|
259
|
+
}
|
|
260
|
+
/** Host-facing handle: `session.keypad`. */
|
|
261
|
+
interface KeypadHandle {
|
|
262
|
+
/** Digits buffered so far (empty when keypad input is not configured). */
|
|
263
|
+
readonly digits: string;
|
|
264
|
+
/** Discard the buffer silently — no event, no message to the model. */
|
|
265
|
+
clear(): void;
|
|
266
|
+
/** Complete the buffered entry now (reason `'submit'`); no-op when empty. */
|
|
267
|
+
submit(): void;
|
|
268
|
+
}
|
|
269
|
+
declare const DEFAULT_KEYPAD_OPTIONS: {
|
|
270
|
+
readonly submitKey: "#";
|
|
271
|
+
readonly clearKey: "*";
|
|
272
|
+
readonly interDigitTimeoutMs: 4000;
|
|
273
|
+
readonly interruptOnKeypress: true;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Appended to the agent instructions when keypad input is enabled. Short and
|
|
277
|
+
* neutral on purpose: what the messages are, not how to run the dialog.
|
|
278
|
+
*/
|
|
279
|
+
declare const DEFAULT_KEYPAD_INSTRUCTIONS: string;
|
|
280
|
+
/** Default text of the user turn injected for a completed entry. */
|
|
281
|
+
declare function defaultKeypadMessage(entry: KeypadEntry): string;
|
|
282
|
+
/** Default text of the user turn injected when the caller presses the clear key. */
|
|
283
|
+
declare const DEFAULT_KEYPAD_CLEAR_MESSAGE = "[keypad] I pressed star — I want to start over and retype from the beginning.";
|
|
284
|
+
/** What a keypress meant to the collector. */
|
|
285
|
+
type KeypadKeyKind = 'digit' | 'submit' | 'clear' | 'ignored';
|
|
286
|
+
interface KeypadCollectorHooks {
|
|
287
|
+
onEntry: (entry: KeypadEntry) => void;
|
|
288
|
+
onClear: (info: {
|
|
289
|
+
discarded: string;
|
|
290
|
+
}) => void;
|
|
291
|
+
}
|
|
292
|
+
declare class KeypadCollector implements KeypadHandle {
|
|
293
|
+
private readonly submitKey;
|
|
294
|
+
private readonly clearKey;
|
|
295
|
+
private readonly interDigitTimeoutMs;
|
|
296
|
+
private readonly maxDigits;
|
|
297
|
+
private readonly hooks;
|
|
298
|
+
private buffer;
|
|
299
|
+
private timer;
|
|
300
|
+
private disposed;
|
|
301
|
+
constructor(options: KeypadOptions, hooks: KeypadCollectorHooks);
|
|
302
|
+
get digits(): string;
|
|
303
|
+
/** Feed one keypress (a Twilio `dtmf` frame). Returns what the key meant. */
|
|
304
|
+
press(key: string): KeypadKeyKind;
|
|
305
|
+
clear(): void;
|
|
306
|
+
submit(): void;
|
|
307
|
+
/** Release the timer; pending digits are dropped (the call is over). */
|
|
308
|
+
dispose(): void;
|
|
309
|
+
private complete;
|
|
310
|
+
private armTimer;
|
|
311
|
+
private cancelTimer;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
196
314
|
//#region src/interruption/InterruptionController.d.ts
|
|
197
315
|
/**
|
|
198
316
|
* InterruptionController — decides whether a barge-in is honored.
|
|
@@ -462,6 +580,15 @@ interface SessionOptions {
|
|
|
462
580
|
* this also serializes ALL mid-call session updates for the session.
|
|
463
581
|
*/
|
|
464
582
|
noiseAdaptiveVad?: NoiseAdaptiveVadOptions;
|
|
583
|
+
/**
|
|
584
|
+
* Opt-in keypad (DTMF) input: buffer keypresses into complete entries —
|
|
585
|
+
* `#` submits, `*` clears, `maxDigits` auto-submits, 4s of no keypresses
|
|
586
|
+
* flushes — stop the agent on every keypress, and inject each entry as a
|
|
587
|
+
* `[keypad] ...` user turn the model answers. Emits `keypad.entry` /
|
|
588
|
+
* `keypad.cleared`; the raw `dtmf` event keeps firing per key. Set `{}` to
|
|
589
|
+
* enable with defaults; unset = raw `dtmf` events only, as before.
|
|
590
|
+
*/
|
|
591
|
+
keypad?: KeypadOptions;
|
|
465
592
|
/**
|
|
466
593
|
* `afterPlayback` (default): tool results wait until current agent audio
|
|
467
594
|
* finishes playing. `immediate`: send as soon as the tool completes.
|
|
@@ -688,9 +815,16 @@ interface SessionEventMap {
|
|
|
688
815
|
'background_audio.stopped': (info: {
|
|
689
816
|
preset?: string;
|
|
690
817
|
}) => void;
|
|
818
|
+
/** Raw keypress (every Twilio dtmf frame). With `keypad` configured it fires AFTER the collector consumed the key. */
|
|
691
819
|
dtmf: (info: {
|
|
692
820
|
digit: string;
|
|
693
821
|
}) => void;
|
|
822
|
+
/** A complete keypad entry (`keypad` option): submit key, `maxDigits`, or inter-digit timeout. */
|
|
823
|
+
'keypad.entry': (entry: KeypadEntry) => void;
|
|
824
|
+
/** The caller pressed the clear key; `discarded` is what the buffer held. */
|
|
825
|
+
'keypad.cleared': (info: {
|
|
826
|
+
discarded: string;
|
|
827
|
+
}) => void;
|
|
694
828
|
'usage.updated': (usage: UsageInfo, delta: ProviderUsage) => void;
|
|
695
829
|
error: (error: Error) => void;
|
|
696
830
|
}
|
|
@@ -729,11 +863,17 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
729
863
|
readonly streamSid: string;
|
|
730
864
|
readonly callInfo: ToolCallInfo;
|
|
731
865
|
readonly context: SessionContext;
|
|
866
|
+
/**
|
|
867
|
+
* Keypad (DTMF) input handle: digits buffered so far, `clear()`, `submit()`.
|
|
868
|
+
* Inert (empty, no-ops) unless the `keypad` session option is configured.
|
|
869
|
+
*/
|
|
870
|
+
readonly keypad: KeypadHandle;
|
|
732
871
|
private stateValue;
|
|
733
872
|
private readonly deps;
|
|
734
873
|
private readonly log;
|
|
735
874
|
private readonly tracker;
|
|
736
875
|
private readonly interruptions;
|
|
876
|
+
private readonly keypadCollector;
|
|
737
877
|
private readonly usageAccumulator;
|
|
738
878
|
private readonly toolQueue;
|
|
739
879
|
private readonly transcriptEntries;
|
|
@@ -863,6 +1003,8 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
863
1003
|
/** Immediate teardown (no goodbye). */
|
|
864
1004
|
end(reason?: CallEndReason): Promise<void>;
|
|
865
1005
|
private buildToolset;
|
|
1006
|
+
/** Agent instructions plus the kit's own notes that must survive handoffs (keypad). */
|
|
1007
|
+
private composeInstructions;
|
|
866
1008
|
private buildProviderInit;
|
|
867
1009
|
private wireTransport;
|
|
868
1010
|
/** Emit 'error'; when the host attached no listener, log instead of losing it. */
|
|
@@ -934,6 +1076,9 @@ declare class CallSession extends TypedEmitter<SessionEventMap> {
|
|
|
934
1076
|
* starts immediately while the provider session is still being built.
|
|
935
1077
|
*/
|
|
936
1078
|
private playPreGreeting;
|
|
1079
|
+
private handleKeypress;
|
|
1080
|
+
private onKeypadEntry;
|
|
1081
|
+
private onKeypadCleared;
|
|
937
1082
|
/** Armed whenever the agent goes quiet and we're waiting on the caller. */
|
|
938
1083
|
private armIdleTimer;
|
|
939
1084
|
private clearIdleTimer;
|
|
@@ -1126,4 +1271,4 @@ declare class PlaybackTracker {
|
|
|
1126
1271
|
private maybeForget;
|
|
1127
1272
|
}
|
|
1128
1273
|
//#endregion
|
|
1129
|
-
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderFallbackInfo, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
|
1274
|
+
export { Agent, type AgentDefinition, type ApprovalRequestInfo, type BackgroundAudioOptions, type BackgroundAudioPreset, type BackgroundAudioSpec, BaseRealtimeProvider, type BridgeConfig, type BridgeEventMap, type BuiltinToolsConfig, type CallEndReason, CallSession, type CallSessionFacade, type CallSnapshot, type CallStartedInfo, type CallState, type CaptureGreetingOptions, type CapturedGreeting, DEFAULT_KEYPAD_CLEAR_MESSAGE, DEFAULT_KEYPAD_INSTRUCTIONS, DEFAULT_KEYPAD_OPTIONS, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, type DeafnessOptions, type FinishCallToolOptions, type GreetingOptions, type HandoffDirective, type HangupOptions, type IdleOptions, InMemorySessionStore, type InferSchema, type InterruptionBlockCause, InterruptionController, type InterruptionDecision, type InterruptionRateLimit, type InterruptionSettings, KeypadCollector, type KeypadCollectorHooks, type KeypadEntry, type KeypadEntryReason, type KeypadHandle, type KeypadKeyKind, type KeypadOptions, type Logger, type MarkEchoResult, NoiseAdaptiveVadController, type NoiseAdaptiveVadOptions, PlaybackTracker, type ProviderAudioDelta, type ProviderCapabilities, type ProviderCloseInfo, type ProviderEvents, type ProviderFactory, type ProviderFactoryContext, type ProviderFallbackInfo, type ProviderSessionInit, type ProviderToolCall, type ProviderToolSchema, type ProviderUsage, type ReconnectPolicy, type SendTextOptions, type SendToolResultOptions, SessionContext, type SessionEventMap, type SessionOptions, type SessionStore, type SessionUpdateOptions, type Tool, type ToolCallInfo, type ToolContext, type ToolDecoration, type ToolDefinition, type ToolMiddleware, type ToolRunInfo, type ToolStrategy, type TranscriptEntry, type TransferCallToolOptions, TwilioRealtimeBridge, type TwilioRestOptions, type TwilioStartEvent, type UsageInfo, type VadAdjustment, type VadConfig, type VadNoiseMetrics, type VadSuggestionInfo, type VadTuningProfile, type WebSocketLike, type ZodSchemaLike, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, defaultKeypadMessage, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
package/dist/index.mjs
CHANGED
|
@@ -189,6 +189,105 @@ function createHandoffTool(target) {
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
//#endregion
|
|
192
|
+
//#region src/dtmf/KeypadCollector.ts
|
|
193
|
+
const DEFAULT_KEYPAD_OPTIONS = {
|
|
194
|
+
submitKey: "#",
|
|
195
|
+
clearKey: "*",
|
|
196
|
+
interDigitTimeoutMs: 4e3,
|
|
197
|
+
interruptOnKeypress: true
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Appended to the agent instructions when keypad input is enabled. Short and
|
|
201
|
+
* neutral on purpose: what the messages are, not how to run the dialog.
|
|
202
|
+
*/
|
|
203
|
+
const DEFAULT_KEYPAD_INSTRUCTIONS = "Keypad input: the caller may type on their phone keypad instead of speaking. Keypresses arrive as a user message starting with \"[keypad]\" that contains the typed digits — treat it as the caller's answer.";
|
|
204
|
+
/** Default text of the user turn injected for a completed entry. */
|
|
205
|
+
function defaultKeypadMessage(entry) {
|
|
206
|
+
const { digits } = entry;
|
|
207
|
+
return `[keypad] I typed on my phone keypad: ${digits} — ${digits.length} digit${digits.length === 1 ? "" : "s"}. Digit by digit: ${[...digits].join(" ")}`;
|
|
208
|
+
}
|
|
209
|
+
/** Default text of the user turn injected when the caller presses the clear key. */
|
|
210
|
+
const DEFAULT_KEYPAD_CLEAR_MESSAGE = "[keypad] I pressed star — I want to start over and retype from the beginning.";
|
|
211
|
+
var KeypadCollector = class {
|
|
212
|
+
submitKey;
|
|
213
|
+
clearKey;
|
|
214
|
+
interDigitTimeoutMs;
|
|
215
|
+
maxDigits;
|
|
216
|
+
hooks;
|
|
217
|
+
buffer = "";
|
|
218
|
+
timer = null;
|
|
219
|
+
disposed = false;
|
|
220
|
+
constructor(options, hooks) {
|
|
221
|
+
this.submitKey = options.submitKey ?? DEFAULT_KEYPAD_OPTIONS.submitKey;
|
|
222
|
+
this.clearKey = options.clearKey ?? DEFAULT_KEYPAD_OPTIONS.clearKey;
|
|
223
|
+
this.interDigitTimeoutMs = options.interDigitTimeoutMs ?? DEFAULT_KEYPAD_OPTIONS.interDigitTimeoutMs;
|
|
224
|
+
this.maxDigits = options.maxDigits !== void 0 && options.maxDigits > 0 ? options.maxDigits : void 0;
|
|
225
|
+
this.hooks = hooks;
|
|
226
|
+
}
|
|
227
|
+
get digits() {
|
|
228
|
+
return this.buffer;
|
|
229
|
+
}
|
|
230
|
+
/** Feed one keypress (a Twilio `dtmf` frame). Returns what the key meant. */
|
|
231
|
+
press(key) {
|
|
232
|
+
if (this.disposed) return "ignored";
|
|
233
|
+
if (key === this.submitKey) {
|
|
234
|
+
this.cancelTimer();
|
|
235
|
+
this.complete("submit");
|
|
236
|
+
return "submit";
|
|
237
|
+
}
|
|
238
|
+
if (key === this.clearKey) {
|
|
239
|
+
this.cancelTimer();
|
|
240
|
+
const discarded = this.buffer;
|
|
241
|
+
this.buffer = "";
|
|
242
|
+
this.hooks.onClear({ discarded });
|
|
243
|
+
return "clear";
|
|
244
|
+
}
|
|
245
|
+
if (!/^[0-9]$/.test(key)) return "ignored";
|
|
246
|
+
this.cancelTimer();
|
|
247
|
+
this.buffer += key;
|
|
248
|
+
if (this.maxDigits !== void 0 && this.buffer.length >= this.maxDigits) this.complete("maxDigits");
|
|
249
|
+
else this.armTimer();
|
|
250
|
+
return "digit";
|
|
251
|
+
}
|
|
252
|
+
clear() {
|
|
253
|
+
this.cancelTimer();
|
|
254
|
+
this.buffer = "";
|
|
255
|
+
}
|
|
256
|
+
submit() {
|
|
257
|
+
if (this.disposed) return;
|
|
258
|
+
this.cancelTimer();
|
|
259
|
+
this.complete("submit");
|
|
260
|
+
}
|
|
261
|
+
/** Release the timer; pending digits are dropped (the call is over). */
|
|
262
|
+
dispose() {
|
|
263
|
+
this.disposed = true;
|
|
264
|
+
this.cancelTimer();
|
|
265
|
+
this.buffer = "";
|
|
266
|
+
}
|
|
267
|
+
complete(reason) {
|
|
268
|
+
if (!this.buffer) return;
|
|
269
|
+
const digits = this.buffer;
|
|
270
|
+
this.buffer = "";
|
|
271
|
+
this.hooks.onEntry({
|
|
272
|
+
digits,
|
|
273
|
+
reason
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
armTimer() {
|
|
277
|
+
this.timer = setTimeout(() => {
|
|
278
|
+
this.timer = null;
|
|
279
|
+
this.complete("timeout");
|
|
280
|
+
}, this.interDigitTimeoutMs);
|
|
281
|
+
this.timer.unref?.();
|
|
282
|
+
}
|
|
283
|
+
cancelTimer() {
|
|
284
|
+
if (this.timer) {
|
|
285
|
+
clearTimeout(this.timer);
|
|
286
|
+
this.timer = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
//#endregion
|
|
192
291
|
//#region src/interruption/InterruptionController.ts
|
|
193
292
|
var InterruptionController = class {
|
|
194
293
|
settings;
|
|
@@ -889,11 +988,17 @@ var CallSession = class extends TypedEmitter {
|
|
|
889
988
|
streamSid;
|
|
890
989
|
callInfo;
|
|
891
990
|
context;
|
|
991
|
+
/**
|
|
992
|
+
* Keypad (DTMF) input handle: digits buffered so far, `clear()`, `submit()`.
|
|
993
|
+
* Inert (empty, no-ops) unless the `keypad` session option is configured.
|
|
994
|
+
*/
|
|
995
|
+
keypad;
|
|
892
996
|
stateValue = "connecting";
|
|
893
997
|
deps;
|
|
894
998
|
log;
|
|
895
999
|
tracker = new PlaybackTracker();
|
|
896
1000
|
interruptions;
|
|
1001
|
+
keypadCollector;
|
|
897
1002
|
usageAccumulator = new UsageAccumulator();
|
|
898
1003
|
toolQueue = new ToolResultQueue();
|
|
899
1004
|
transcriptEntries = [];
|
|
@@ -967,6 +1072,18 @@ var CallSession = class extends TypedEmitter {
|
|
|
967
1072
|
this.activeAgentValue = deps.agent;
|
|
968
1073
|
this.context = new SessionContext(deps.options.context);
|
|
969
1074
|
this.interruptions = new InterruptionController(deps.options.interruptions);
|
|
1075
|
+
this.keypadCollector = deps.options.keypad ? new KeypadCollector(deps.options.keypad, {
|
|
1076
|
+
onEntry: (entry) => this.onKeypadEntry(entry),
|
|
1077
|
+
onClear: (info) => this.onKeypadCleared(info)
|
|
1078
|
+
}) : null;
|
|
1079
|
+
const collector = this.keypadCollector;
|
|
1080
|
+
this.keypad = {
|
|
1081
|
+
get digits() {
|
|
1082
|
+
return collector?.digits ?? "";
|
|
1083
|
+
},
|
|
1084
|
+
clear: () => collector?.clear(),
|
|
1085
|
+
submit: () => collector?.submit()
|
|
1086
|
+
};
|
|
970
1087
|
const params = deps.start.start.customParameters ?? {};
|
|
971
1088
|
this.callInfo = {
|
|
972
1089
|
direction: params.direction === "outbound" ? "outbound" : "inbound",
|
|
@@ -1208,9 +1325,16 @@ var CallSession = class extends TypedEmitter {
|
|
|
1208
1325
|
}
|
|
1209
1326
|
return tools;
|
|
1210
1327
|
}
|
|
1328
|
+
/** Agent instructions plus the kit's own notes that must survive handoffs (keypad). */
|
|
1329
|
+
composeInstructions() {
|
|
1330
|
+
const base = this.activeAgentValue.resolveInstructions(this.context);
|
|
1331
|
+
const keypad = this.deps.options.keypad;
|
|
1332
|
+
if (!keypad || keypad.instructions === false) return base;
|
|
1333
|
+
return `${base}\n\n${keypad.instructions ?? "Keypad input: the caller may type on their phone keypad instead of speaking. Keypresses arrive as a user message starting with \"[keypad]\" that contains the typed digits — treat it as the caller's answer."}`;
|
|
1334
|
+
}
|
|
1211
1335
|
buildProviderInit() {
|
|
1212
1336
|
return {
|
|
1213
|
-
instructions: this.
|
|
1337
|
+
instructions: this.composeInstructions() + (this.pregreeting ? `\n\nYou already opened the call by saying: "${this.pregreeting.text}". Do not greet again — continue the conversation from there.` : ""),
|
|
1214
1338
|
voice: this.activeAgentValue.voice,
|
|
1215
1339
|
vad: this.vadOverride !== void 0 ? this.vadOverride : this.deps.options.vad,
|
|
1216
1340
|
bridgeOwnsInterruptions: true,
|
|
@@ -1233,7 +1357,9 @@ var CallSession = class extends TypedEmitter {
|
|
|
1233
1357
|
transport.on("dtmf", (event) => {
|
|
1234
1358
|
this.clearIdleTimer();
|
|
1235
1359
|
this.nudgeCount = 0;
|
|
1236
|
-
|
|
1360
|
+
const digit = event.dtmf.digit;
|
|
1361
|
+
this.handleKeypress(digit);
|
|
1362
|
+
this.emit("dtmf", { digit });
|
|
1237
1363
|
});
|
|
1238
1364
|
transport.on("stop", () => void this.teardown("caller-hangup"));
|
|
1239
1365
|
transport.on("close", () => void this.teardown("caller-hangup"));
|
|
@@ -2007,7 +2133,7 @@ var CallSession = class extends TypedEmitter {
|
|
|
2007
2133
|
if (!(!this.provider.capabilities.sessionUpdate || voiceChanges && !this.provider.capabilities.voiceChangeMidSession && this.deps.options.handoffVoicePolicy === "reconnect")) {
|
|
2008
2134
|
if (voiceChanges && !this.provider.capabilities.voiceChangeMidSession) this.log.warn(`agent "${target.id}" declares voice "${target.voice}" but the provider cannot change voice mid-session — keeping the current voice (set handoffVoicePolicy: 'reconnect' to switch)`);
|
|
2009
2135
|
await this.provider.updateSession({
|
|
2010
|
-
instructions: this.
|
|
2136
|
+
instructions: this.composeInstructions(),
|
|
2011
2137
|
tools: this.buildProviderInit().tools,
|
|
2012
2138
|
providerOptions: target.providerOptions
|
|
2013
2139
|
});
|
|
@@ -2083,6 +2209,29 @@ var CallSession = class extends TypedEmitter {
|
|
|
2083
2209
|
this.transcriptEntries.push(entry);
|
|
2084
2210
|
this.emit("transcript.agent", entry);
|
|
2085
2211
|
}
|
|
2212
|
+
handleKeypress(key) {
|
|
2213
|
+
if (!this.keypadCollector) return;
|
|
2214
|
+
if (this.deps.options.keypad?.interruptOnKeypress !== false) this.interrupt();
|
|
2215
|
+
this.keypadCollector.press(key);
|
|
2216
|
+
}
|
|
2217
|
+
onKeypadEntry(entry) {
|
|
2218
|
+
this.emit("keypad.entry", entry);
|
|
2219
|
+
const message = this.deps.options.keypad?.message;
|
|
2220
|
+
if (message === false) return;
|
|
2221
|
+
this.provider?.sendText((message ?? defaultKeypadMessage)(entry), {
|
|
2222
|
+
role: "user",
|
|
2223
|
+
triggerResponse: true
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
onKeypadCleared(info) {
|
|
2227
|
+
this.emit("keypad.cleared", info);
|
|
2228
|
+
const clearMessage = this.deps.options.keypad?.clearMessage;
|
|
2229
|
+
if (clearMessage === false) return;
|
|
2230
|
+
this.provider?.sendText(clearMessage ?? "[keypad] I pressed star — I want to start over and retype from the beginning.", {
|
|
2231
|
+
role: "user",
|
|
2232
|
+
triggerResponse: true
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2086
2235
|
/** Armed whenever the agent goes quiet and we're waiting on the caller. */
|
|
2087
2236
|
armIdleTimer() {
|
|
2088
2237
|
const idle = this.deps.options.idle;
|
|
@@ -2142,6 +2291,7 @@ var CallSession = class extends TypedEmitter {
|
|
|
2142
2291
|
for (const timer of this.timers) clearTimeout(timer);
|
|
2143
2292
|
this.timers.clear();
|
|
2144
2293
|
this.clearIdleTimer();
|
|
2294
|
+
this.keypadCollector?.dispose();
|
|
2145
2295
|
this.bgAudio.stop({ immediate: true });
|
|
2146
2296
|
for (const controller of this.runningTools.values()) controller.abort(/* @__PURE__ */ new Error("call ended"));
|
|
2147
2297
|
this.runningTools.clear();
|
|
@@ -2449,4 +2599,4 @@ async function captureGreetingAudio(options) {
|
|
|
2449
2599
|
});
|
|
2450
2600
|
}
|
|
2451
2601
|
//#endregion
|
|
2452
|
-
export { Agent, BaseRealtimeProvider, CallSession, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, InMemorySessionStore, InterruptionController, NoiseAdaptiveVadController, PlaybackTracker, SessionContext, TwilioRealtimeBridge, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
|
2602
|
+
export { Agent, BaseRealtimeProvider, CallSession, DEFAULT_KEYPAD_CLEAR_MESSAGE, DEFAULT_KEYPAD_INSTRUCTIONS, DEFAULT_KEYPAD_OPTIONS, DEFAULT_RECONNECT_POLICY, DEFAULT_SESSION_OPTIONS, InMemorySessionStore, InterruptionController, KeypadCollector, NoiseAdaptiveVadController, PlaybackTracker, SessionContext, TwilioRealtimeBridge, captureGreetingAudio, collectAgentGraph, composeExecution, connectStreamTwiml, consoleLogger, createFinishCallTool, createHandoffTool, createTransferCallTool, decorateTool, defaultKeypadMessage, emptyUsage, handoffToolName, isHandoffDirective, noopLogger, resolveSessionOptions, tool, zodToJsonSchema };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "realtime-voice-agents",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Provider-agnostic bridge between Twilio Media Streams and realtime speech-to-speech AI APIs (OpenAI Realtime, xAI Grok Voice, Gemini Live). Multi-agent handoffs, Zod tools with execution strategies, mark-based playback tracking, interruption guards, and hold audio — for Node.js voice agents over the phone.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twilio",
|