[{"data":1,"prerenderedAt":879},["ShallowReactive",2],{"blog:/blog/voice-agent-with-tools-twilio":3},{"id":4,"title":5,"author":6,"body":7,"category":868,"date":869,"description":870,"excerpt":871,"extension":872,"meta":873,"navigation":234,"path":874,"readTime":875,"seo":876,"stem":877,"__hash__":878},"blog/blog/voice-agent-with-tools-twilio.md","Building a Voice AI Agent That Can Actually Do Things","Henco Burger",{"type":8,"value":9,"toc":857},"minimark",[10,14,17,22,25,28,31,34,38,41,44,112,115,118,122,125,128,142,145,148,186,189,193,196,199,557,564,568,573,576,579,583,586,633,636,643,646,649,653,656,788,796,799,803,806,809,813,850,853],[11,12,13],"p",{},"There is a specific failure mode in voice AI that anyone who has shipped one will recognise. The agent sounds great. It is warm, it is fast, it handles interruptions. And then the caller asks it to change a booking, and it says \"I have updated that for you,\" and nothing has been updated, because the model on the phone has no tools and no database and no idea whether that was even possible.",[11,15,16],{},"This is not a prompting problem. It is a budget problem, and the budget is time.",[18,19,21],"h2",{"id":20},"why-the-model-on-the-phone-is-small","Why the model on the phone is small",[11,23,24],{},"A phone call has a rhythm. Somewhere past about a second of silence, a caller starts to think the line has dropped. That gives you roughly one second, total, to transcribe what they said, decide what to say back, synthesise it, and get the first audio byte onto the wire.",[11,26,27],{},"That budget picks your model for you. It has to be small and fast, which is exactly right for conversation and exactly wrong for judgement. A small model is a poor choice for deciding whether a booking can be moved, what a refund policy allows, or whether this caller is who they claim to be.",[11,29,30],{},"The work that matters runs on a completely different clock. Looking a customer up, changing a record, waiting for a human to approve something: five to thirty seconds, sometimes minutes. None of that fits inside a one second budget, so it cannot live inside the call.",[11,32,33],{},"It has to be something the call talks to. And talking to it has to be asynchronous, because the caller is still on the line while it happens.",[18,35,37],{"id":36},"the-split","The split",[11,39,40],{},"NoLag is realtime messaging infrastructure and a coordination layer for AI agents: pub/sub topics, rooms, presence and delivery guarantees, with higher-level SDKs for the patterns agents actually need. For voice, that means a live phone call can become a room that other software joins.",[11,42,43],{},"Three pieces, each doing one job:",[45,46,47,63],"table",{},[48,49,50],"thead",{},[51,52,53,57,60],"tr",{},[54,55,56],"th",{},"Piece",[54,58,59],{},"Job",[54,61,62],{},"Clock",[64,65,66,85,101],"tbody",{},[51,67,68,79,82],{},[69,70,71],"td",{},[72,73,75],"a",{"href":74},"/docs/sdks/voice-engine",[76,77,78],"code",{},"@nolag/voice-engine",[69,80,81],{},"Turn taking, interruption, voice activity detection, filler speech",[69,83,84],{},"Milliseconds",[51,86,87,95,98],{},[69,88,89],{},[72,90,92],{"href":91},"/docs/high-level-sdks/voice",[76,93,94],{},"@nolag/voice",[69,96,97],{},"Puts the call in a NoLag room, carries the ask and the answer",[69,99,100],{},"Per turn",[51,102,103,106,109],{},[69,104,105],{},"Your orchestrator",[69,107,108],{},"Bigger model, real tools, database credentials",[69,110,111],{},"Seconds to minutes",[11,113,114],{},"The voice engine has no NoLag dependency and no opinion about which telephony provider carries the call or which models do the thinking. Those are interfaces. What it owns is the part that no prompt can fix: deciding when someone has stopped talking, who holds the channel, and what the caller actually hears.",[11,116,117],{},"The orchestrator is a separate process because that is the honest shape of it. It holds database credentials the phone-facing server does not need, it scales on a different curve, and it can be restarted without dropping a call.",[18,119,121],{"id":120},"waiting-out-loud-is-the-feature","Waiting out loud is the feature",[11,123,124],{},"Here is the part that took the longest to get right, and it is a design decision rather than an implementation detail.",[11,126,127],{},"When the model on the phone needs help, it asks the orchestrator. Most of the time that answer will not come back inside a turn. So the turn ends, the agent says something honest, and the answer is spoken whenever it arrives.",[129,130,131],"code-tabs",{},[132,133,140],"pre",{"className":134,"code":136,"filename":137,"language":138,"meta":139},[135],"language-text","CALLER  Hi, it's Alex. My flight landed early, can you move my pickup?\nAGENT   Let me have a look.\nAGENT   Let me check that for you, one moment.\nAGENT   Still checking on that.\nAGENT   Yes, I can move your airport pickup earlier. You're currently\n        booked for three forty pm, and I have one fifteen pm or one\n        forty-five pm available.\n","What the caller hears","text","",[76,141,136],{"__ignoreMap":139},[11,143,144],{},"That transcript is from a real call against our example app, transcribed back from the audio the caller would actually have heard. The two tool calls behind it, looking the booking up and checking availability, took about eight seconds. Nothing survives being nested inside a turn for eight seconds, so it was not nested inside one.",[11,146,147],{},"The behaviour has three tiers, and only the first one is invisible to the caller:",[45,149,150,160],{},[48,151,152],{},[51,153,154,157],{},[54,155,156],{},"Answer arrives",[54,158,159],{},"What the caller experiences",[64,161,162,170,178],{},[51,163,164,167],{},[69,165,166],{},"Within the grace window (1200ms by default)",[69,168,169],{},"Folded into the reply. They never learn anything was asked",[51,171,172,175],{},[69,173,174],{},"Later",[69,176,177],{},"An acknowledgement now, the answer spoken in the next gap",[51,179,180,183],{},[69,181,182],{},"Never",[69,184,185],{},"A spoken apology, never silence",[11,187,188],{},"An answer that arrives late is never spliced into audio that is already playing, and never spoken over the caller. There is one audio buffer to the far end, so two things speaking at once are not heard as two voices. They are heard as alternating fragments of two sentences. If the floor never frees, the answer is dropped rather than said late, because talking over someone is worse than staying quiet and the next ordinary turn can usually carry the point anyway.",[18,190,192],{"id":191},"one-tool-not-twenty","One tool, not twenty",[11,194,195],{},"The agent on the phone gets exactly one tool: ask the orchestrator. That is deliberate.",[11,197,198],{},"A small model handed twenty tools has to choose between them, and choosing badly is where small models fail. Handed one, its only judgement is \"do I need help, and how do I phrase it\". Which of the orchestrator's tools to actually reach for is decided by the orchestrator, which is large enough to decide well.",[129,200,201,389],{},[132,202,207],{"className":203,"code":204,"filename":205,"language":206,"meta":139,"style":139},"language-typescript shiki shiki-themes github-light github-dark","import { NoLagVoice, orchestratedModel } from '@nolag/voice'\n\nconst bridge = new NoLagVoice({ agents }).orchestrator()\nawait bridge.ready()\n\nconst session = new VoiceSession({\n  transport,\n  providers: {\n    ...providers,\n    llm: orchestratedModel({\n      model: providers.llm,   // the fast model that carries the conversation\n      bridge,\n      floor: session,         // so an answer can be spoken outside a turn\n      callId\n    })\n  },\n  systemPrompt\n})\n","On the call server","typescript",[76,208,209,229,236,265,279,284,302,308,314,323,334,344,350,359,365,371,377,383],{"__ignoreMap":139},[210,211,214,218,222,225],"span",{"class":212,"line":213},"line",1,[210,215,217],{"class":216},"szBVR","import",[210,219,221],{"class":220},"sVt8B"," { NoLagVoice, orchestratedModel } ",[210,223,224],{"class":216},"from",[210,226,228],{"class":227},"sZZnC"," '@nolag/voice'\n",[210,230,232],{"class":212,"line":231},2,[210,233,235],{"emptyLinePlaceholder":234},true,"\n",[210,237,239,242,246,249,252,256,259,262],{"class":212,"line":238},3,[210,240,241],{"class":216},"const",[210,243,245],{"class":244},"sj4cs"," bridge",[210,247,248],{"class":216}," =",[210,250,251],{"class":216}," new",[210,253,255],{"class":254},"sScJk"," NoLagVoice",[210,257,258],{"class":220},"({ agents }).",[210,260,261],{"class":254},"orchestrator",[210,263,264],{"class":220},"()\n",[210,266,268,271,274,277],{"class":212,"line":267},4,[210,269,270],{"class":216},"await",[210,272,273],{"class":220}," bridge.",[210,275,276],{"class":254},"ready",[210,278,264],{"class":220},[210,280,282],{"class":212,"line":281},5,[210,283,235],{"emptyLinePlaceholder":234},[210,285,287,289,292,294,296,299],{"class":212,"line":286},6,[210,288,241],{"class":216},[210,290,291],{"class":244}," session",[210,293,248],{"class":216},[210,295,251],{"class":216},[210,297,298],{"class":254}," VoiceSession",[210,300,301],{"class":220},"({\n",[210,303,305],{"class":212,"line":304},7,[210,306,307],{"class":220},"  transport,\n",[210,309,311],{"class":212,"line":310},8,[210,312,313],{"class":220},"  providers: {\n",[210,315,317,320],{"class":212,"line":316},9,[210,318,319],{"class":216},"    ...",[210,321,322],{"class":220},"providers,\n",[210,324,326,329,332],{"class":212,"line":325},10,[210,327,328],{"class":220},"    llm: ",[210,330,331],{"class":254},"orchestratedModel",[210,333,301],{"class":220},[210,335,337,340],{"class":212,"line":336},11,[210,338,339],{"class":220},"      model: providers.llm,   ",[210,341,343],{"class":342},"sJ8bj","// the fast model that carries the conversation\n",[210,345,347],{"class":212,"line":346},12,[210,348,349],{"class":220},"      bridge,\n",[210,351,353,356],{"class":212,"line":352},13,[210,354,355],{"class":220},"      floor: session,         ",[210,357,358],{"class":342},"// so an answer can be spoken outside a turn\n",[210,360,362],{"class":212,"line":361},14,[210,363,364],{"class":220},"      callId\n",[210,366,368],{"class":212,"line":367},15,[210,369,370],{"class":220},"    })\n",[210,372,374],{"class":212,"line":373},16,[210,375,376],{"class":220},"  },\n",[210,378,380],{"class":212,"line":379},17,[210,381,382],{"class":220},"  systemPrompt\n",[210,384,386],{"class":212,"line":385},18,[210,387,388],{"class":220},"})\n",[132,390,393],{"className":203,"code":391,"filename":392,"language":206,"meta":139,"style":139},"import { Handoff } from '@nolag/agents'\nimport { ORCHESTRATOR_CAPABILITY, ORCHESTRATOR_ROOM } from '@nolag/voice'\n\nnew Handoff(agents.room(ORCHESTRATOR_ROOM))\n  .onTask([ORCHESTRATOR_CAPABILITY], async (task, respond) => {\n    const { question, context, callId } = task.payload\n    const answer = await yourBigModelWithTools(question, context)\n\n    respond('success', { speech: answer.speech, detail: answer.detail })\n  })\n","On the orchestrator",[76,394,395,407,418,422,445,487,517,535,539,552],{"__ignoreMap":139},[210,396,397,399,402,404],{"class":212,"line":213},[210,398,217],{"class":216},[210,400,401],{"class":220}," { Handoff } ",[210,403,224],{"class":216},[210,405,406],{"class":227}," '@nolag/agents'\n",[210,408,409,411,414,416],{"class":212,"line":231},[210,410,217],{"class":216},[210,412,413],{"class":220}," { ORCHESTRATOR_CAPABILITY, ORCHESTRATOR_ROOM } ",[210,415,224],{"class":216},[210,417,228],{"class":227},[210,419,420],{"class":212,"line":238},[210,421,235],{"emptyLinePlaceholder":234},[210,423,424,427,430,433,436,439,442],{"class":212,"line":267},[210,425,426],{"class":216},"new",[210,428,429],{"class":254}," Handoff",[210,431,432],{"class":220},"(agents.",[210,434,435],{"class":254},"room",[210,437,438],{"class":220},"(",[210,440,441],{"class":244},"ORCHESTRATOR_ROOM",[210,443,444],{"class":220},"))\n",[210,446,447,450,453,456,459,462,465,468,472,475,478,481,484],{"class":212,"line":281},[210,448,449],{"class":220},"  .",[210,451,452],{"class":254},"onTask",[210,454,455],{"class":220},"([",[210,457,458],{"class":244},"ORCHESTRATOR_CAPABILITY",[210,460,461],{"class":220},"], ",[210,463,464],{"class":216},"async",[210,466,467],{"class":220}," (",[210,469,471],{"class":470},"s4XuR","task",[210,473,474],{"class":220},", ",[210,476,477],{"class":470},"respond",[210,479,480],{"class":220},") ",[210,482,483],{"class":216},"=>",[210,485,486],{"class":220}," {\n",[210,488,489,492,495,498,500,503,505,508,511,514],{"class":212,"line":286},[210,490,491],{"class":216},"    const",[210,493,494],{"class":220}," { ",[210,496,497],{"class":244},"question",[210,499,474],{"class":220},[210,501,502],{"class":244},"context",[210,504,474],{"class":220},[210,506,507],{"class":244},"callId",[210,509,510],{"class":220}," } ",[210,512,513],{"class":216},"=",[210,515,516],{"class":220}," task.payload\n",[210,518,519,521,524,526,529,532],{"class":212,"line":304},[210,520,491],{"class":216},[210,522,523],{"class":244}," answer",[210,525,248],{"class":216},[210,527,528],{"class":216}," await",[210,530,531],{"class":254}," yourBigModelWithTools",[210,533,534],{"class":220},"(question, context)\n",[210,536,537],{"class":212,"line":310},[210,538,235],{"emptyLinePlaceholder":234},[210,540,541,544,546,549],{"class":212,"line":316},[210,542,543],{"class":254},"    respond",[210,545,438],{"class":220},[210,547,548],{"class":227},"'success'",[210,550,551],{"class":220},", { speech: answer.speech, detail: answer.detail })\n",[210,553,554],{"class":212,"line":325},[210,555,556],{"class":220},"  })\n",[11,558,559,560,563],{},"The ",[76,561,562],{},"speech"," field is read to the caller and may be heard a turn or two after the question, so it has to stand on its own. \"The 1:15 is free\" is not enough. \"About that pickup, the 1:15 is free\" is.",[18,565,567],{"id":566},"why-the-ask-is-text-and-not-a-function-call","Why the ask is text and not a function call",[11,569,570,572],{},[76,571,331],{}," has the conversational model ask in-band, by replying with a marker that gets filtered out of what is spoken, rather than through a function-calling API. That looks like a shortcut until you look at the clock again.",[11,574,575],{},"The voice engine streams. Synthesis of the first finished sentence starts while the model is still writing, and that head start is most of what makes an agent feel responsive. A function-calling round trip has to complete before you know whether it was a tool call or an answer, which throws that head start away on every single turn, including the overwhelming majority that never need help at all.",[11,577,578],{},"In-band, a turn that does not ask costs exactly nothing. It also works with providers that have no tool-calling API. The cost is that a small model occasionally garbles the marker, which fails safe: an unrecognised marker is stripped from speech rather than read aloud.",[18,580,582],{"id":581},"scaling-the-orchestrator","Scaling the orchestrator",[11,584,585],{},"Orchestrators are the expensive half, so they are the half you scale. NoLag distributes work across a pool with load-balanced subscriptions, where each task reaches exactly one member of the group.",[129,587,588],{},[132,589,592],{"className":203,"code":590,"filename":591,"language":206,"meta":139,"style":139},"import { orchestratorPoolOptions } from '@nolag/voice'\n\nconst client = NoLag(token, { url, ...orchestratorPoolOptions() })\n","TypeScript",[76,593,594,605,609],{"__ignoreMap":139},[210,595,596,598,601,603],{"class":212,"line":213},[210,597,217],{"class":216},[210,599,600],{"class":220}," { orchestratorPoolOptions } ",[210,602,224],{"class":216},[210,604,228],{"class":227},[210,606,607],{"class":212,"line":231},[210,608,235],{"emptyLinePlaceholder":234},[210,610,611,613,616,618,621,624,627,630],{"class":212,"line":238},[210,612,241],{"class":216},[210,614,615],{"class":244}," client",[210,617,248],{"class":216},[210,619,620],{"class":254}," NoLag",[210,622,623],{"class":220},"(token, { url, ",[210,625,626],{"class":216},"...",[210,628,629],{"class":254},"orchestratorPoolOptions",[210,631,632],{"class":220},"() })\n",[11,634,635],{},"Two details are worth knowing, because both fail quietly rather than loudly.",[11,637,638,639,642],{},"The load-balance group defaults to the actor token id, so replicas holding different tokens each form a group of one and each still receive a copy. That means every replica runs the same expensive inference and several answers race back to the same call. The only symptom is the bill, which is why ",[76,640,641],{},"orchestratorPoolOptions()"," exists: it sets both halves.",[11,644,645],{},"And every process needs its own actor token. The broker never delivers a message back to the actor that published it, so an orchestrator sharing a token with the call server never sees a task at all. Everything else looks healthy: both connect, both appear in presence, the capability is discovered exactly as it should be. The asks simply time out.",[11,647,648],{},"In our own testing a two replica pool split eight tasks four and four with no duplicates, and when one replica was taken away the survivor picked up all of the next batch.",[18,650,652],{"id":651},"watching-a-call-while-it-happens","Watching a call while it happens",[11,654,655],{},"Because the call is a room, anything with access can join it. A supervisor dashboard can stream the transcript turn by turn, with per-leg latency, and send a line back that the agent speaks to the caller seconds later.",[129,657,658],{},[132,659,661],{"className":203,"code":660,"filename":591,"language":206,"meta":139,"style":139},"const watcher = voice.watchCall(callId, {\n  onTranscript: (line) => render(line.role, line.text),\n  onEvent: (event) => {\n    if (event.event === 'turn-complete') showLatency(event.totalMs)\n  }\n})\n\nwatcher.say('Let them know we can hold the car until two.')\nwatcher.instruct('Only discuss today\\'s booking.')\n",[76,662,663,681,701,717,739,744,748,752,768],{"__ignoreMap":139},[210,664,665,667,670,672,675,678],{"class":212,"line":213},[210,666,241],{"class":216},[210,668,669],{"class":244}," watcher",[210,671,248],{"class":216},[210,673,674],{"class":220}," voice.",[210,676,677],{"class":254},"watchCall",[210,679,680],{"class":220},"(callId, {\n",[210,682,683,686,689,691,693,695,698],{"class":212,"line":231},[210,684,685],{"class":254},"  onTranscript",[210,687,688],{"class":220},": (",[210,690,212],{"class":470},[210,692,480],{"class":220},[210,694,483],{"class":216},[210,696,697],{"class":254}," render",[210,699,700],{"class":220},"(line.role, line.text),\n",[210,702,703,706,708,711,713,715],{"class":212,"line":238},[210,704,705],{"class":254},"  onEvent",[210,707,688],{"class":220},[210,709,710],{"class":470},"event",[210,712,480],{"class":220},[210,714,483],{"class":216},[210,716,486],{"class":220},[210,718,719,722,725,728,731,733,736],{"class":212,"line":267},[210,720,721],{"class":216},"    if",[210,723,724],{"class":220}," (event.event ",[210,726,727],{"class":216},"===",[210,729,730],{"class":227}," 'turn-complete'",[210,732,480],{"class":220},[210,734,735],{"class":254},"showLatency",[210,737,738],{"class":220},"(event.totalMs)\n",[210,740,741],{"class":212,"line":281},[210,742,743],{"class":220},"  }\n",[210,745,746],{"class":212,"line":286},[210,747,388],{"class":220},[210,749,750],{"class":212,"line":304},[210,751,235],{"emptyLinePlaceholder":234},[210,753,754,757,760,762,765],{"class":212,"line":310},[210,755,756],{"class":220},"watcher.",[210,758,759],{"class":254},"say",[210,761,438],{"class":220},[210,763,764],{"class":227},"'Let them know we can hold the car until two.'",[210,766,767],{"class":220},")\n",[210,769,770,772,775,777,780,783,786],{"class":212,"line":316},[210,771,756],{"class":220},[210,773,774],{"class":254},"instruct",[210,776,438],{"class":220},[210,778,779],{"class":227},"'Only discuss today",[210,781,782],{"class":244},"\\'",[210,784,785],{"class":227},"s booking.'",[210,787,767],{"class":220},[11,789,790,792,793,795],{},[76,791,759],{}," is spoken to the caller immediately. ",[76,794,774],{}," is never spoken: it is guidance the model sees from its next turn onward, which is what you want for \"keep it brief\" or \"stop offering refunds\".",[11,797,798],{},"There is one rule that catches everyone here. A watcher needs its own actor token, because the broker never delivers a message back to the actor that published it. A dashboard sharing the call's token connects perfectly happily and then displays nothing at all.",[18,800,802],{"id":801},"building-without-a-phone-number","Building without a phone number",[11,804,805],{},"Iterating on turn taking through real phone calls is slow and costs money every attempt. The voice engine ships a browser simulator that speaks the same Media Streams protocol a carrier does, at the same endpoint, so the session cannot tell it apart from a real call.",[11,807,808],{},"That matters more than convenience. Barge-in and turn taking cannot be checked from a unit test, because they are timing behaviour between a person and a stream. Being able to talk over your agent in a browser tab, twenty times in a row, is how you find out that it stops when you interrupt it.",[18,810,812],{"id":811},"what-to-read-next","What to read next",[814,815,816,824,831],"ul",{},[817,818,819,823],"li",{},[72,820,821],{"href":74},[76,822,78],{}," for the real-time half: turn taking, barge-in, filler speech, call screener and voicemail handling, recording",[817,825,826,830],{},[72,827,828],{"href":91},[76,829,94],{}," for the coordination half: the room, the orchestrator bridge, supervisor steering",[817,832,833,839,840,474,843,846,847],{},[72,834,836],{"href":835},"/docs/agents",[76,837,838],{},"@nolag/agents"," for the six coordination patterns the orchestrator side is built from, including ",[76,841,842],{},"Handoff",[76,844,845],{},"Tools"," and ",[76,848,849],{},"Approve",[11,851,852],{},"Together they are a complete voice agent: fast reflexes on the phone, real capability behind it, and a caller who is told the truth about how long something is taking.",[854,855,856],"style",{},"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":139,"searchDepth":231,"depth":231,"links":858},[859,860,861,862,863,864,865,866,867],{"id":20,"depth":231,"text":21},{"id":36,"depth":231,"text":37},{"id":120,"depth":231,"text":121},{"id":191,"depth":231,"text":192},{"id":566,"depth":231,"text":567},{"id":581,"depth":231,"text":582},{"id":651,"depth":231,"text":652},{"id":801,"depth":231,"text":802},{"id":811,"depth":231,"text":812},"AI Agents","2026-08-31","A voice agent has about a second to reply, so the model answering the phone is small and has no tools. Here is how to give it real capability over NoLag: a Twilio call becomes a coordination room, and a larger orchestrator answers asynchronously while the caller keeps talking.",null,"md",{},"/blog/voice-agent-with-tools-twilio","12 min read",{"title":5,"description":870},"blog/voice-agent-with-tools-twilio","y8mf49CkbZuQdwbtt8QexrImKvHl8Ne7YqwJaRWdt8Y",1788160341663]