[{"data":1,"prerenderedAt":1647},["ShallowReactive",2],{"docs:/docs/sdks/voice-engine":3},{"id":4,"title":5,"body":6,"description":1640,"extension":1641,"meta":1642,"navigation":203,"path":1643,"seo":1644,"stem":1645,"__hash__":1646},"docs/docs/sdks/voice-engine.md","Voice Engine SDK",{"type":7,"value":8,"toc":1620},"minimark",[9,14,18,23,29,32,43,48,70,74,103,122,126,129,478,481,485,488,689,704,708,712,914,936,940,943,1096,1106,1113,1116,1127,1131,1134,1198,1201,1212,1316,1320,1323,1387,1405,1411,1415,1418,1537,1542,1547,1551,1558,1564,1570,1576,1582,1588,1592,1595,1599,1613,1616],[10,11,13],"h1",{"id":12},"nolagvoice-engine","@nolag/voice-engine",[15,16,17],"p",{},"The real-time half of a voice agent: turn taking, interruption, voice activity detection, filler speech, call-screener handling and recording.",[19,20,22],"h2",{"id":21},"overview","Overview",[15,24,25,28],{},[26,27,13],"code",{}," is the one NoLag package with no NoLag dependency. It has no opinion about which telephony provider carries the call, which models do the thinking, or whether anything is coordinating from outside. Those are interfaces.",[15,30,31],{},"What it owns is the part that is genuinely hard, and that no prompt can fix: deciding when someone has stopped talking, who holds the channel, and what the caller actually hears.",[15,33,34,35,42],{},"It is one half of a voice agent. This half has to answer in about a second, so it stays fast and knows nothing. Pair it with ",[36,37,39],"a",{"href":38},"/docs/high-level-sdks/voice",[26,40,41],{},"@nolag/voice"," and the call gains the other half: a room through which it reaches a larger orchestrator for knowledge and tool calls, and a human who can watch or approve.",[44,45,47],"h3",{"id":46},"key-features","Key Features",[49,50,51,55,58,61,64,67],"ul",{},[52,53,54],"li",{},"Voice activity detection that adapts to the room rather than using a fixed threshold",[52,56,57],{},"Barge-in: the caller can interrupt the agent mid-sentence, and the agent's own voice cannot interrupt itself",[52,59,60],{},"Filler speech (\"let me have a look\") to cover the wait while a turn is generated",[52,62,63],{},"Call screeners and voicemail recognised and answered from a script, never from the model",[52,65,66],{},"Recording: time-aligned caller and agent audio, plus a transcript and per-turn latency",[52,68,69],{},"A browser simulator, so you can build a voice agent without a phone number",[19,71,73],{"id":72},"installation","Installation",[75,76,77],"code-tabs",{},[78,79,85],"pre",{"className":80,"code":81,"filename":82,"language":83,"meta":84,"style":84},"language-bash shiki shiki-themes github-light github-dark","npm install @nolag/voice-engine\n","Terminal","bash","",[26,86,87],{"__ignoreMap":84},[88,89,92,96,100],"span",{"class":90,"line":91},"line",1,[88,93,95],{"class":94},"sScJk","npm",[88,97,99],{"class":98},"sZZnC"," install",[88,101,102],{"class":98}," @nolag/voice-engine\n",[104,105,107],"note",{"title":106},"Runs anywhere modern JavaScript does",[15,108,109,110,113,114,117,118,121],{},"The main entry point touches no filesystem, no HTTP server and no socket library, and uses no Node ",[26,111,112],{},"Buffer",": audio framing, WAV and base64 are all ",[26,115,116],{},"Uint8Array"," and ",[26,119,120],{},"DataView",". It runs unchanged on Node, Deno, Bun, Workers and in a browser. Recording and the simulator are separate imports, because those genuinely do need the filesystem and a server.",[19,123,125],{"id":124},"quick-start","Quick Start",[15,127,128],{},"Everything below is one complete program. Run it, open the page, and talk to the agent. No telephony account, no phone number, no public tunnel.",[75,130,131],{},[78,132,137],{"className":133,"code":134,"filename":135,"language":136,"meta":84,"style":84},"language-typescript shiki shiki-themes github-light github-dark","import {\n  VoiceSession,\n  OpenRouterSpeechToText,\n  OpenRouterLanguageModel,\n  OpenRouterTextToSpeech\n} from '@nolag/voice-engine'\nimport { startSimulator } from '@nolag/voice-engine/simulator'\n\nconst apiKey = process.env.OPENROUTER_API_KEY\n\n// One key covers all three legs here, but each is an interface.\nconst providers = {\n  stt: new OpenRouterSpeechToText({ apiKey, model: 'openai/gpt-4o-mini-transcribe' }),\n  llm: new OpenRouterLanguageModel({ apiKey, model: 'mistralai/ministral-8b-2512' }),\n  tts: new OpenRouterTextToSpeech({\n    apiKey,\n    model: 'deepgram/aura-2',\n    voice: 'aura-2-thalia-en',\n    sampleRate: 24000   // the rate this model emits, not the phone line\n  })\n}\n\nawait startSimulator({\n  port: 3000,\n  onCall: (transport) => {\n    new VoiceSession({\n      transport,\n      providers,\n      systemPrompt:\n        'You are a friendly phone assistant. Keep replies to one or two short ' +\n        'spoken sentences. Never use markdown or lists.',\n      lines: { greeting: 'Hi, how can I help?' }\n    })\n  }\n})\n","TypeScript","typescript",[26,138,139,149,155,161,167,173,185,198,205,224,229,236,248,269,287,301,307,319,330,342,348,354,359,370,381,402,413,419,425,431,440,448,460,466,472],{"__ignoreMap":84},[88,140,141,145],{"class":90,"line":91},[88,142,144],{"class":143},"szBVR","import",[88,146,148],{"class":147},"sVt8B"," {\n",[88,150,152],{"class":90,"line":151},2,[88,153,154],{"class":147},"  VoiceSession,\n",[88,156,158],{"class":90,"line":157},3,[88,159,160],{"class":147},"  OpenRouterSpeechToText,\n",[88,162,164],{"class":90,"line":163},4,[88,165,166],{"class":147},"  OpenRouterLanguageModel,\n",[88,168,170],{"class":90,"line":169},5,[88,171,172],{"class":147},"  OpenRouterTextToSpeech\n",[88,174,176,179,182],{"class":90,"line":175},6,[88,177,178],{"class":147},"} ",[88,180,181],{"class":143},"from",[88,183,184],{"class":98}," '@nolag/voice-engine'\n",[88,186,188,190,193,195],{"class":90,"line":187},7,[88,189,144],{"class":143},[88,191,192],{"class":147}," { startSimulator } ",[88,194,181],{"class":143},[88,196,197],{"class":98}," '@nolag/voice-engine/simulator'\n",[88,199,201],{"class":90,"line":200},8,[88,202,204],{"emptyLinePlaceholder":203},true,"\n",[88,206,208,211,215,218,221],{"class":90,"line":207},9,[88,209,210],{"class":143},"const",[88,212,214],{"class":213},"sj4cs"," apiKey",[88,216,217],{"class":143}," =",[88,219,220],{"class":147}," process.env.",[88,222,223],{"class":213},"OPENROUTER_API_KEY\n",[88,225,227],{"class":90,"line":226},10,[88,228,204],{"emptyLinePlaceholder":203},[88,230,232],{"class":90,"line":231},11,[88,233,235],{"class":234},"sJ8bj","// One key covers all three legs here, but each is an interface.\n",[88,237,239,241,244,246],{"class":90,"line":238},12,[88,240,210],{"class":143},[88,242,243],{"class":213}," providers",[88,245,217],{"class":143},[88,247,148],{"class":147},[88,249,251,254,257,260,263,266],{"class":90,"line":250},13,[88,252,253],{"class":147},"  stt: ",[88,255,256],{"class":143},"new",[88,258,259],{"class":94}," OpenRouterSpeechToText",[88,261,262],{"class":147},"({ apiKey, model: ",[88,264,265],{"class":98},"'openai/gpt-4o-mini-transcribe'",[88,267,268],{"class":147}," }),\n",[88,270,272,275,277,280,282,285],{"class":90,"line":271},14,[88,273,274],{"class":147},"  llm: ",[88,276,256],{"class":143},[88,278,279],{"class":94}," OpenRouterLanguageModel",[88,281,262],{"class":147},[88,283,284],{"class":98},"'mistralai/ministral-8b-2512'",[88,286,268],{"class":147},[88,288,290,293,295,298],{"class":90,"line":289},15,[88,291,292],{"class":147},"  tts: ",[88,294,256],{"class":143},[88,296,297],{"class":94}," OpenRouterTextToSpeech",[88,299,300],{"class":147},"({\n",[88,302,304],{"class":90,"line":303},16,[88,305,306],{"class":147},"    apiKey,\n",[88,308,310,313,316],{"class":90,"line":309},17,[88,311,312],{"class":147},"    model: ",[88,314,315],{"class":98},"'deepgram/aura-2'",[88,317,318],{"class":147},",\n",[88,320,322,325,328],{"class":90,"line":321},18,[88,323,324],{"class":147},"    voice: ",[88,326,327],{"class":98},"'aura-2-thalia-en'",[88,329,318],{"class":147},[88,331,333,336,339],{"class":90,"line":332},19,[88,334,335],{"class":147},"    sampleRate: ",[88,337,338],{"class":213},"24000",[88,340,341],{"class":234},"   // the rate this model emits, not the phone line\n",[88,343,345],{"class":90,"line":344},20,[88,346,347],{"class":147},"  })\n",[88,349,351],{"class":90,"line":350},21,[88,352,353],{"class":147},"}\n",[88,355,357],{"class":90,"line":356},22,[88,358,204],{"emptyLinePlaceholder":203},[88,360,362,365,368],{"class":90,"line":361},23,[88,363,364],{"class":143},"await",[88,366,367],{"class":94}," startSimulator",[88,369,300],{"class":147},[88,371,373,376,379],{"class":90,"line":372},24,[88,374,375],{"class":147},"  port: ",[88,377,378],{"class":213},"3000",[88,380,318],{"class":147},[88,382,384,387,390,394,397,400],{"class":90,"line":383},25,[88,385,386],{"class":94},"  onCall",[88,388,389],{"class":147},": (",[88,391,393],{"class":392},"s4XuR","transport",[88,395,396],{"class":147},") ",[88,398,399],{"class":143},"=>",[88,401,148],{"class":147},[88,403,405,408,411],{"class":90,"line":404},26,[88,406,407],{"class":143},"    new",[88,409,410],{"class":94}," VoiceSession",[88,412,300],{"class":147},[88,414,416],{"class":90,"line":415},27,[88,417,418],{"class":147},"      transport,\n",[88,420,422],{"class":90,"line":421},28,[88,423,424],{"class":147},"      providers,\n",[88,426,428],{"class":90,"line":427},29,[88,429,430],{"class":147},"      systemPrompt:\n",[88,432,434,437],{"class":90,"line":433},30,[88,435,436],{"class":98},"        'You are a friendly phone assistant. Keep replies to one or two short '",[88,438,439],{"class":143}," +\n",[88,441,443,446],{"class":90,"line":442},31,[88,444,445],{"class":98},"        'spoken sentences. Never use markdown or lists.'",[88,447,318],{"class":147},[88,449,451,454,457],{"class":90,"line":450},32,[88,452,453],{"class":147},"      lines: { greeting: ",[88,455,456],{"class":98},"'Hi, how can I help?'",[88,458,459],{"class":147}," }\n",[88,461,463],{"class":90,"line":462},33,[88,464,465],{"class":147},"    })\n",[88,467,469],{"class":90,"line":468},34,[88,470,471],{"class":147},"  }\n",[88,473,475],{"class":90,"line":474},35,[88,476,477],{"class":147},"})\n",[15,479,480],{},"The simulator page speaks the same Media Streams protocol a carrier does, at the same endpoint, so the session cannot tell it apart from a real call. That matters more than convenience: barge-in and turn taking cannot be checked from a unit test, and iterating on them through real phone calls is slow and costs money every attempt.",[19,482,484],{"id":483},"taking-real-calls","Taking real calls",[15,486,487],{},"Twilio Media Streams is included as a transport. The webhook answers with TwiML pointing at your WebSocket, and each socket becomes one session.",[75,489,490],{},[78,491,493],{"className":133,"code":492,"filename":135,"language":136,"meta":84,"style":84},"import { WebSocketServer } from 'ws'\nimport { TwilioMediaStreamTransport, VoiceSession } from '@nolag/voice-engine'\n\n// Answer the voice webhook with TwiML, where PUBLIC_HOST is reachable from the\n// internet. The \u003CParameter> tags become CallInfo on the session:\n//   \u003CResponse>\u003CConnect>\n//     \u003CStream url=\"wss://PUBLIC_HOST/media\">\n//       \u003CParameter name=\"peer\" value=\"+61400000000\" />\n//       \u003CParameter name=\"outbound\" value=\"0\" />\n//     \u003C/Stream>\n//   \u003C/Connect>\u003C/Response>\n\nconst wss = new WebSocketServer({ server, path: '/media' })\n\nwss.on('connection', (socket) => {\n  const transport = new TwilioMediaStreamTransport({ socket })\n  const session = new VoiceSession({ transport, providers, systemPrompt, lines })\n  socket.on('close', () => session.close('socket closed'))\n})\n",[26,494,495,507,518,522,527,532,537,542,547,552,557,562,566,590,594,620,638,654,685],{"__ignoreMap":84},[88,496,497,499,502,504],{"class":90,"line":91},[88,498,144],{"class":143},[88,500,501],{"class":147}," { WebSocketServer } ",[88,503,181],{"class":143},[88,505,506],{"class":98}," 'ws'\n",[88,508,509,511,514,516],{"class":90,"line":151},[88,510,144],{"class":143},[88,512,513],{"class":147}," { TwilioMediaStreamTransport, VoiceSession } ",[88,515,181],{"class":143},[88,517,184],{"class":98},[88,519,520],{"class":90,"line":157},[88,521,204],{"emptyLinePlaceholder":203},[88,523,524],{"class":90,"line":163},[88,525,526],{"class":234},"// Answer the voice webhook with TwiML, where PUBLIC_HOST is reachable from the\n",[88,528,529],{"class":90,"line":169},[88,530,531],{"class":234},"// internet. The \u003CParameter> tags become CallInfo on the session:\n",[88,533,534],{"class":90,"line":175},[88,535,536],{"class":234},"//   \u003CResponse>\u003CConnect>\n",[88,538,539],{"class":90,"line":187},[88,540,541],{"class":234},"//     \u003CStream url=\"wss://PUBLIC_HOST/media\">\n",[88,543,544],{"class":90,"line":200},[88,545,546],{"class":234},"//       \u003CParameter name=\"peer\" value=\"+61400000000\" />\n",[88,548,549],{"class":90,"line":207},[88,550,551],{"class":234},"//       \u003CParameter name=\"outbound\" value=\"0\" />\n",[88,553,554],{"class":90,"line":226},[88,555,556],{"class":234},"//     \u003C/Stream>\n",[88,558,559],{"class":90,"line":231},[88,560,561],{"class":234},"//   \u003C/Connect>\u003C/Response>\n",[88,563,564],{"class":90,"line":238},[88,565,204],{"emptyLinePlaceholder":203},[88,567,568,570,573,575,578,581,584,587],{"class":90,"line":250},[88,569,210],{"class":143},[88,571,572],{"class":213}," wss",[88,574,217],{"class":143},[88,576,577],{"class":143}," new",[88,579,580],{"class":94}," WebSocketServer",[88,582,583],{"class":147},"({ server, path: ",[88,585,586],{"class":98},"'/media'",[88,588,589],{"class":147}," })\n",[88,591,592],{"class":90,"line":271},[88,593,204],{"emptyLinePlaceholder":203},[88,595,596,599,602,605,608,611,614,616,618],{"class":90,"line":289},[88,597,598],{"class":147},"wss.",[88,600,601],{"class":94},"on",[88,603,604],{"class":147},"(",[88,606,607],{"class":98},"'connection'",[88,609,610],{"class":147},", (",[88,612,613],{"class":392},"socket",[88,615,396],{"class":147},[88,617,399],{"class":143},[88,619,148],{"class":147},[88,621,622,625,628,630,632,635],{"class":90,"line":303},[88,623,624],{"class":143},"  const",[88,626,627],{"class":213}," transport",[88,629,217],{"class":143},[88,631,577],{"class":143},[88,633,634],{"class":94}," TwilioMediaStreamTransport",[88,636,637],{"class":147},"({ socket })\n",[88,639,640,642,645,647,649,651],{"class":90,"line":309},[88,641,624],{"class":143},[88,643,644],{"class":213}," session",[88,646,217],{"class":143},[88,648,577],{"class":143},[88,650,410],{"class":94},[88,652,653],{"class":147},"({ transport, providers, systemPrompt, lines })\n",[88,655,656,659,661,663,666,669,671,674,677,679,682],{"class":90,"line":321},[88,657,658],{"class":147},"  socket.",[88,660,601],{"class":94},[88,662,604],{"class":147},[88,664,665],{"class":98},"'close'",[88,667,668],{"class":147},", () ",[88,670,399],{"class":143},[88,672,673],{"class":147}," session.",[88,675,676],{"class":94},"close",[88,678,604],{"class":147},[88,680,681],{"class":98},"'socket closed'",[88,683,684],{"class":147},"))\n",[88,686,687],{"class":90,"line":332},[88,688,477],{"class":147},[104,690,693],{"title":691,"type":692},"Observe, do not re-register","warning",[15,694,695,696,699,700,703],{},"The session registers the transport's handlers in its own constructor, and a transport holds one handler per event. Calling ",[26,697,698],{},"transport.onStart()"," yourself replaces the session's and the call is never greeted. Use the ",[26,701,702],{},"observer"," option instead, which is what it is for.",[19,705,707],{"id":706},"configuration","Configuration",[44,709,711],{"id":710},"voicesession","VoiceSession",[713,714,715,731],"table",{},[716,717,718],"thead",{},[719,720,721,725,728],"tr",{},[722,723,724],"th",{},"Option",[722,726,727],{},"Type",[722,729,730],{},"Description",[732,733,734,749,764,779,794,809,824,840,854,870,884,899],"tbody",{},[719,735,736,741,746],{},[737,738,739],"td",{},[26,740,393],{},[737,742,743],{},[26,744,745],{},"AudioTransport",[737,747,748],{},"Required. Where audio comes from and goes",[719,750,751,756,761],{},[737,752,753],{},[26,754,755],{},"providers",[737,757,758],{},[26,759,760],{},"{ stt, llm, tts }",[737,762,763],{},"Required. The three AI legs",[719,765,766,771,776],{},[737,767,768],{},[26,769,770],{},"systemPrompt",[737,772,773],{},[26,774,775],{},"string",[737,777,778],{},"Required. Ask for one or two short spoken sentences",[719,780,781,786,791],{},[737,782,783],{},[26,784,785],{},"lines",[737,787,788],{},[26,789,790],{},"ScriptedLines",[737,792,793],{},"Fixed things it says",[719,795,796,801,806],{},[737,797,798],{},[26,799,800],{},"detector",[737,802,803],{},[26,804,805],{},"UtteranceDetectorOptions",[737,807,808],{},"Listening behaviour",[719,810,811,816,821],{},[737,812,813],{},[26,814,815],{},"fillers",[737,817,818],{},[26,819,820],{},"FillerBank | null",[737,822,823],{},"Stalling phrases",[719,825,826,831,837],{},[737,827,828],{},[26,829,830],{},"fillerDelayMs",[737,832,833,836],{},[26,834,835],{},"number"," (500)",[737,838,839],{},"Wait this long for a real answer before filling",[719,841,842,847,851],{},[737,843,844],{},[26,845,846],{},"language",[737,848,849],{},[26,850,775],{},[737,852,853],{},"Transcription hint",[719,855,856,861,867],{},[737,857,858],{},[26,859,860],{},"waitForHello",[737,862,863,866],{},[26,864,865],{},"boolean"," (true)",[737,868,869],{},"Outbound: hold the intro until they speak",[719,871,872,876,881],{},[737,873,874],{},[26,875,702],{},[737,877,878],{},[26,879,880],{},"SessionObserver",[737,882,883],{},"Everything that happens",[719,885,886,891,896],{},[737,887,888],{},[26,889,890],{},"policy",[737,892,893],{},[26,894,895],{},"SessionPolicy",[737,897,898],{},"Replace how a transcript is judged before the model sees it",[719,900,901,906,911],{},[737,902,903],{},[26,904,905],{},"maxDeferred",[737,907,908,910],{},[26,909,835],{}," (2)",[737,912,913],{},"How many unprompted lines may wait for a gap at once",[15,915,916,917,920,921,924,925,117,928,931,932,935],{},"Methods: ",[26,918,919],{},"say(text, options?)"," speaks something as soon as there is room to, ",[26,922,923],{},"instruct(text)"," adds silent guidance the model sees from its next turn, ",[26,926,927],{},"speakUnprompted(speech)",[26,929,930],{},"beginStall(options)"," are below, ",[26,933,934],{},"close(reason)"," ends the session.",[19,937,939],{"id":938},"saying-something-the-caller-did-not-ask-for","Saying something the caller did not ask for",[15,941,942],{},"An answer that arrives from outside the conversation has nowhere to go in a request-response model, and writing it straight to the transport is not an option: there is one audio buffer to the far end, so a second voice is spliced into whatever is already playing.",[75,944,945],{},[78,946,948],{"className":133,"code":947,"filename":135,"language":136,"meta":84,"style":84},"// Waits for a gap. Never talks over the caller or over itself.\nconst line = session.speakUnprompted({\n  text: 'About that pickup, the 1:15 is free.',\n  expiresInMs: 20000    // drop it rather than say it late\n})\n\nif (await line.done !== 'spoken') {\n  // 'interrupted' | 'cancelled' | 'expired' | 'superseded' | 'closed'\n}\n\n// Keep the caller company while something slow happens.\nconst stall = session.beginStall({\n  lines: ['Still checking on that.', 'The system is a bit slow today.'],\n  everyMs: 6000\n})\nstall.stop()\n",[26,949,950,955,971,981,992,996,1000,1022,1027,1031,1035,1040,1056,1073,1081,1085],{"__ignoreMap":84},[88,951,952],{"class":90,"line":91},[88,953,954],{"class":234},"// Waits for a gap. Never talks over the caller or over itself.\n",[88,956,957,959,962,964,966,969],{"class":90,"line":151},[88,958,210],{"class":143},[88,960,961],{"class":213}," line",[88,963,217],{"class":143},[88,965,673],{"class":147},[88,967,968],{"class":94},"speakUnprompted",[88,970,300],{"class":147},[88,972,973,976,979],{"class":90,"line":157},[88,974,975],{"class":147},"  text: ",[88,977,978],{"class":98},"'About that pickup, the 1:15 is free.'",[88,980,318],{"class":147},[88,982,983,986,989],{"class":90,"line":163},[88,984,985],{"class":147},"  expiresInMs: ",[88,987,988],{"class":213},"20000",[88,990,991],{"class":234},"    // drop it rather than say it late\n",[88,993,994],{"class":90,"line":169},[88,995,477],{"class":147},[88,997,998],{"class":90,"line":175},[88,999,204],{"emptyLinePlaceholder":203},[88,1001,1002,1005,1008,1010,1013,1016,1019],{"class":90,"line":187},[88,1003,1004],{"class":143},"if",[88,1006,1007],{"class":147}," (",[88,1009,364],{"class":143},[88,1011,1012],{"class":147}," line.done ",[88,1014,1015],{"class":143},"!==",[88,1017,1018],{"class":98}," 'spoken'",[88,1020,1021],{"class":147},") {\n",[88,1023,1024],{"class":90,"line":200},[88,1025,1026],{"class":234},"  // 'interrupted' | 'cancelled' | 'expired' | 'superseded' | 'closed'\n",[88,1028,1029],{"class":90,"line":207},[88,1030,353],{"class":147},[88,1032,1033],{"class":90,"line":226},[88,1034,204],{"emptyLinePlaceholder":203},[88,1036,1037],{"class":90,"line":231},[88,1038,1039],{"class":234},"// Keep the caller company while something slow happens.\n",[88,1041,1042,1044,1047,1049,1051,1054],{"class":90,"line":238},[88,1043,210],{"class":143},[88,1045,1046],{"class":213}," stall",[88,1048,217],{"class":143},[88,1050,673],{"class":147},[88,1052,1053],{"class":94},"beginStall",[88,1055,300],{"class":147},[88,1057,1058,1061,1064,1067,1070],{"class":90,"line":250},[88,1059,1060],{"class":147},"  lines: [",[88,1062,1063],{"class":98},"'Still checking on that.'",[88,1065,1066],{"class":147},", ",[88,1068,1069],{"class":98},"'The system is a bit slow today.'",[88,1071,1072],{"class":147},"],\n",[88,1074,1075,1078],{"class":90,"line":271},[88,1076,1077],{"class":147},"  everyMs: ",[88,1079,1080],{"class":213},"6000\n",[88,1082,1083],{"class":90,"line":289},[88,1084,477],{"class":147},[88,1086,1087,1090,1093],{"class":90,"line":303},[88,1088,1089],{"class":147},"stall.",[88,1091,1092],{"class":94},"stop",[88,1094,1095],{"class":147},"()\n",[15,1097,1098,1101,1102,1105],{},[26,1099,1100],{},"say()"," is built on this, so a supervisor's line waits for a gap instead of talking over the caller. ",[26,1103,1104],{},"say(text, { interrupt: true })"," cuts in, for when stopping the call matters more than being polite.",[15,1107,1108,1109,1112],{},"A barge-in does not discard a waiting line. Cleared audio is stale, but the reason for saying something is not: the caller asked, cut in, and the answer still arrives afterwards. Use ",[26,1110,1111],{},"expiresInMs"," for things that genuinely stop being worth saying.",[15,1114,1115],{},"A stall is not a filler. A filler is one short phrase covering the ordinary gap before a reply; a stall covers a wait long enough that silence reads as a dropped call, and it escalates, because the second thing you say to someone who has been waiting cannot be the first thing again. Rungs are skipped rather than queued when someone is talking.",[104,1117,1119],{"title":1118},"Where the answers come from",[15,1120,1121,1122,1126],{},"The engine never learns the word \"tool\". Pair it with ",[36,1123,1124],{"href":38},[26,1125,41],{},", which gives the call one async tool: an orchestrator with a bigger model and real tools, reached over a NoLag room, whose answers arrive through exactly these primitives.",[44,1128,1130],{"id":1129},"scripted-lines","Scripted lines",[15,1132,1133],{},"Said verbatim, with no model call, because the moments they cover cannot afford a surprise or a delay.",[713,1135,1136,1146],{},[716,1137,1138],{},[719,1139,1140,1143],{},[722,1141,1142],{},"Line",[722,1144,1145],{},"When",[732,1147,1148,1158,1168,1178,1188],{},[719,1149,1150,1155],{},[737,1151,1152],{},[26,1153,1154],{},"greeting",[737,1156,1157],{},"Answering an inbound call",[719,1159,1160,1165],{},[737,1161,1162],{},[26,1163,1164],{},"outboundGreeting",[737,1166,1167],{},"A call you placed, held until they speak first",[719,1169,1170,1175],{},[737,1171,1172],{},[26,1173,1174],{},"identify",[737,1176,1177],{},"A screener asks who is calling. Says nothing about the customer",[719,1179,1180,1185],{},[737,1181,1182],{},[26,1183,1184],{},"voicemail",[737,1186,1187],{},"Left on an answering machine, after which the call ends",[719,1189,1190,1195],{},[737,1191,1192],{},[26,1193,1194],{},"farewell",[737,1196,1197],{},"The caller signals the conversation is over, then it hangs up",[44,1199,808],{"id":1200},"listening-behaviour",[15,1202,1203,1204,1207,1208,1211],{},"The detector adapts to the room, so most calls need none of this. Reach for it when the agent replies to nothing (raise ",[26,1205,1206],{},"noiseMultiplier","), never hears a quiet speaker (lower it), or cuts people off mid-thought (raise ",[26,1209,1210],{},"silenceHangMs",", at the cost of adding that delay to every reply).",[713,1213,1214,1225],{},[716,1215,1216],{},[719,1217,1218,1220,1223],{},[722,1219,724],{},[722,1221,1222],{},"Default",[722,1224,730],{},[732,1226,1227,1240,1252,1264,1277,1290,1303],{},[719,1228,1229,1234,1237],{},[737,1230,1231],{},[26,1232,1233],{},"speechRms",[737,1235,1236],{},"500",[737,1238,1239],{},"Absolute floor. The real threshold adapts above it",[719,1241,1242,1246,1249],{},[737,1243,1244],{},[26,1245,1206],{},[737,1247,1248],{},"3",[737,1250,1251],{},"Speech must beat the measured noise floor by this",[719,1253,1254,1258,1261],{},[737,1255,1256],{},[26,1257,1210],{},[737,1259,1260],{},"700",[737,1262,1263],{},"Silence that ends a turn. Added to every reply's delay",[719,1265,1266,1271,1274],{},[737,1267,1268],{},[26,1269,1270],{},"bargeInFrames",[737,1272,1273],{},"5",[737,1275,1276],{},"20 ms frames of speech needed to interrupt the agent",[719,1278,1279,1284,1287],{},[737,1280,1281],{},[26,1282,1283],{},"bargeInMultiplier",[737,1285,1286],{},"1.5",[737,1288,1289],{},"How much louder than the threshold an interruption must be",[719,1291,1292,1297,1300],{},[737,1293,1294],{},[26,1295,1296],{},"minUtteranceMs",[737,1298,1299],{},"400",[737,1301,1302],{},"Shorter than this is a blip, not a turn",[719,1304,1305,1310,1313],{},[737,1306,1307],{},[26,1308,1309],{},"maxUtteranceMs",[737,1311,1312],{},"15000",[737,1314,1315],{},"Force-closes a runaway utterance",[19,1317,1319],{"id":1318},"recording","Recording",[15,1321,1322],{},"A separate import, because it needs the filesystem, and opt-in, because nothing is captured unless you pass one.",[75,1324,1325],{},[78,1326,1328],{"className":133,"code":1327,"filename":135,"language":136,"meta":84,"style":84},"import { createRecorder } from '@nolag/voice-engine/recorder'\n\nconst recorder = createRecorder({ dir: './recordings', callId: 'CA123' })\nnew VoiceSession({ ...options, observer: recorder })\n",[26,1329,1330,1342,1346,1372],{"__ignoreMap":84},[88,1331,1332,1334,1337,1339],{"class":90,"line":91},[88,1333,144],{"class":143},[88,1335,1336],{"class":147}," { createRecorder } ",[88,1338,181],{"class":143},[88,1340,1341],{"class":98}," '@nolag/voice-engine/recorder'\n",[88,1343,1344],{"class":90,"line":151},[88,1345,204],{"emptyLinePlaceholder":203},[88,1347,1348,1350,1353,1355,1358,1361,1364,1367,1370],{"class":90,"line":157},[88,1349,210],{"class":143},[88,1351,1352],{"class":213}," recorder",[88,1354,217],{"class":143},[88,1356,1357],{"class":94}," createRecorder",[88,1359,1360],{"class":147},"({ dir: ",[88,1362,1363],{"class":98},"'./recordings'",[88,1365,1366],{"class":147},", callId: ",[88,1368,1369],{"class":98},"'CA123'",[88,1371,589],{"class":147},[88,1373,1374,1376,1378,1381,1384],{"class":90,"line":163},[88,1375,256],{"class":143},[88,1377,410],{"class":94},[88,1379,1380],{"class":147},"({ ",[88,1382,1383],{"class":143},"...",[88,1385,1386],{"class":147},"options, observer: recorder })\n",[15,1388,1389,1390,1066,1393,1396,1397,1400,1401,1404],{},"Per call it writes ",[26,1391,1392],{},"\u003CcallId>-caller.wav",[26,1394,1395],{},"\u003CcallId>-agent.wav",", a ",[26,1398,1399],{},".jsonl"," event log with per-turn latency, and a readable ",[26,1402,1403],{},".txt"," transcript. The two tracks are time-aligned, so they play side by side, and audio is streamed to disk rather than buffered, so a long call costs no more memory than a short one.",[104,1406,1408],{"title":1407,"type":692},"Recording is personal data",[15,1409,1410],{},"Those files are call audio and transcripts of identifiable people, written in the clear with no retention policy. In many places recording needs the consent of everyone on the call, which is separate from disclosing that they are talking to an AI. It is off by default deliberately.",[19,1412,1414],{"id":1413},"bringing-your-own-vendors","Bringing your own vendors",[15,1416,1417],{},"Implement three interfaces and the engine neither knows nor cares who is behind them:",[75,1419,1420],{},[78,1421,1423],{"className":133,"code":1422,"filename":135,"language":136,"meta":84,"style":84},"interface SpeechToText { transcribe(req): Promise\u003Cstring> }\ninterface LanguageModel { chat(req): Promise\u003Cstring> }   // streams sentences\ninterface TextToSpeech  { readonly sampleRate: number; speak(req): Promise\u003Cvoid> }\n",[26,1424,1425,1461,1493],{"__ignoreMap":84},[88,1426,1427,1430,1433,1436,1439,1441,1444,1447,1450,1453,1456,1458],{"class":90,"line":91},[88,1428,1429],{"class":143},"interface",[88,1431,1432],{"class":94}," SpeechToText",[88,1434,1435],{"class":147}," { ",[88,1437,1438],{"class":94},"transcribe",[88,1440,604],{"class":147},[88,1442,1443],{"class":392},"req",[88,1445,1446],{"class":147},")",[88,1448,1449],{"class":143},":",[88,1451,1452],{"class":94}," Promise",[88,1454,1455],{"class":147},"\u003C",[88,1457,775],{"class":213},[88,1459,1460],{"class":147},"> }\n",[88,1462,1463,1465,1468,1470,1473,1475,1477,1479,1481,1483,1485,1487,1490],{"class":90,"line":151},[88,1464,1429],{"class":143},[88,1466,1467],{"class":94}," LanguageModel",[88,1469,1435],{"class":147},[88,1471,1472],{"class":94},"chat",[88,1474,604],{"class":147},[88,1476,1443],{"class":392},[88,1478,1446],{"class":147},[88,1480,1449],{"class":143},[88,1482,1452],{"class":94},[88,1484,1455],{"class":147},[88,1486,775],{"class":213},[88,1488,1489],{"class":147},"> }   ",[88,1491,1492],{"class":234},"// streams sentences\n",[88,1494,1495,1497,1500,1503,1506,1509,1511,1514,1517,1520,1522,1524,1526,1528,1530,1532,1535],{"class":90,"line":157},[88,1496,1429],{"class":143},[88,1498,1499],{"class":94}," TextToSpeech",[88,1501,1502],{"class":147},"  { ",[88,1504,1505],{"class":143},"readonly",[88,1507,1508],{"class":392}," sampleRate",[88,1510,1449],{"class":143},[88,1512,1513],{"class":213}," number",[88,1515,1516],{"class":147},"; ",[88,1518,1519],{"class":94},"speak",[88,1521,604],{"class":147},[88,1523,1443],{"class":392},[88,1525,1446],{"class":147},[88,1527,1449],{"class":143},[88,1529,1452],{"class":94},[88,1531,1455],{"class":147},[88,1533,1534],{"class":213},"void",[88,1536,1460],{"class":147},[15,1538,1539,1541],{},[26,1540,1519],{}," streams deliberately: the caller hears the first byte, so time to first byte is what governs how responsive the agent feels, and the gap between providers is measured in seconds rather than milliseconds.",[15,1543,1544,1546],{},[26,1545,745],{}," is the same idea for telephony. Implement it and any carrier, or a browser, or a test double, can drive a session.",[19,1548,1550],{"id":1549},"what-it-does-that-is-not-obvious","What it does that is not obvious",[15,1552,1553,1557],{},[1554,1555,1556],"strong",{},"Voice detection adapts to the room."," A fixed energy threshold cannot serve both a quiet phone line and a noisy room. In a room noisier than the fixed floor, a static threshold does not merely misfire, it latches open permanently and streams noise to your transcription bill.",[15,1559,1560,1563],{},[1554,1561,1562],{},"Interruption is judged independently of utterance state."," The obvious implementation triggers barge-in when an utterance begins, which fails silently: if noise or the tail of the caller's own sentence already opened one, the opening transition never comes again and the caller can never interrupt for the rest of the call.",[15,1565,1566,1569],{},[1554,1567,1568],{},"Playback is tracked by mark, not by clock."," Audio goes out far faster than it is heard, so only the far end knows when the agent has finished speaking. A turn can contain several clips, so marks are counted rather than treated as a flag.",[15,1571,1572,1575],{},[1554,1573,1574],{},"The model is the last resort, not the first."," Noise, screeners and goodbyes are recognised and answered from a script before the model is consulted. Instructing a model not to do something works most of the time, which is another way of saying it fails in front of customers. Constraining it at the boundary works every time.",[15,1577,1578,1581],{},[1554,1579,1580],{},"Speaking starts on the first sentence."," The reply is streamed and synthesis of the first finished sentence begins while the model is still writing. Synthesis latency is dominated by fixed per-request overhead rather than text length, so splitting a reply into many small clips makes it slower, not faster.",[15,1583,1584,1587],{},[1554,1585,1586],{},"One clip at a time, whoever asked for it."," Audio is serialised separately from turns, because the work queue is held for the whole of a slow model call, which is exactly when something else needs to speak. Two clips streaming at once are not heard as two voices; they are heard as alternating fragments of two sentences.",[19,1589,1591],{"id":1590},"where-to-run-it","Where to run it",[15,1593,1594],{},"A phone call is a long-lived WebSocket, which request-oriented platforms cannot hold. AWS Lambda and Cloud Functions will not work directly, so the natural targets are Cloud Run, Fargate, or Lambda behind API Gateway WebSockets. Match the platform to the socket first, then enjoy the small bundle for cold starts.",[19,1596,1598],{"id":1597},"coordination","Coordination",[15,1600,1601,1602,1606,1607,1609,1610,1612],{},"Pair it with ",[36,1603,1604],{"href":38},[26,1605,41],{}," and each call becomes a NoLag room, so dashboards, supervisors and other agents can watch a call and steer it while it is happening. ",[26,1608,711],{}," takes an ",[26,1611,702],{},", and that package's publisher is shaped to be one, so the two clip together without either knowing much about the other.",[15,1614,1615],{},"Together they are a complete voice agent: fast reflexes on the phone, real capability behind it.",[1617,1618,1619],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":84,"searchDepth":151,"depth":151,"links":1621},[1622,1625,1626,1627,1628,1631,1635,1636,1637,1638,1639],{"id":21,"depth":151,"text":22,"children":1623},[1624],{"id":46,"depth":157,"text":47},{"id":72,"depth":151,"text":73},{"id":124,"depth":151,"text":125},{"id":483,"depth":151,"text":484},{"id":706,"depth":151,"text":707,"children":1629},[1630],{"id":710,"depth":157,"text":711},{"id":938,"depth":151,"text":939,"children":1632},[1633,1634],{"id":1129,"depth":157,"text":1130},{"id":1200,"depth":157,"text":808},{"id":1318,"depth":151,"text":1319},{"id":1413,"depth":151,"text":1414},{"id":1549,"depth":151,"text":1550},{"id":1590,"depth":151,"text":1591},{"id":1597,"depth":151,"text":1598},"Real-time voice agent engine: turn taking, barge-in, voice activity detection, filler speech and call-screener handling, independent of any telephony or AI vendor.","md",{},"/docs/sdks/voice-engine",{"title":5,"description":1640},"docs/sdks/voice-engine","Jgc1ucMRbuzCkJRtHspL0ReYFXuuiFhDhJQPgiq01KY",1788160344762]