LLM agents
An LLM agent answers your callers with a large language model: it sends what the caller said to the model and speaks the reply. A project created from the LLM agent template keeps its settings in two files: voicethere.llm.json and prompt.md. The LLM tab of the project edits those files with a form. A fuller guide to each setting is coming; the sections below say what each field does.
How the LLM tab works
The tab reads the files from your project code and writes them back when you press Save. Nothing is stored anywhere else, so an edit in the Code tab and an edit in the LLM tab change the same text. If the code changed after you opened the page, Save is refused and you reload first.
Provider
Which company's API answers your callers: OpenAI, Anthropic, Google Gemini or OpenRouter. Each one needs its own API key.
Model
The model name your provider expects. OpenAI, Anthropic and OpenRouter have a default, so the field can stay empty. Gemini has none, so you set one. New projects from the LLM agent template start on OpenRouter with anthropic/claude-haiku-5.5, a fast, low-cost model that calls tools reliably, and read the key from the AGENT_OPENROUTER_API_KEY secret. Existing projects keep the provider and model in their own voicethere.llm.json. For production, pick a model from OpenRouter's list after checking its price, speed and tool support. Temperature and the maximum reply length sit next to it.
API key secret
The name of a project secret that holds your key, never the key itself. The name has to start with AGENT_, because only those secrets reach the agent. Add the secret under Settings, in Environment and secrets.
Prompt
The system prompt stored in prompt.md. Callers hear every word, so ask for short sentences without lists or special characters.
Greeting
What the agent says first. By default callers cannot interrupt it; a switch lets them.
Barge-in
Whether a caller can cut the agent off. Project default follows the barge-in setting of the project.
Speech during an announcement
What to do with words a caller says while a message that cannot be interrupted is playing. Answer after waits for the message to end and then replies. Ignore drops those words.
Interruption note
When a caller cuts a reply off, a short note goes into the history so the model knows the caller did not hear all of it.
History
How many earlier turns the model sees on each reply. More turns remember more and cost more.
Interrupt policy
Cancel stops the model request as soon as the caller interrupts. Finish lets it complete, so tool calls that already started are not lost.
Tool rounds
The most times the model may call tools before it has to answer. It keeps one reply from looping.
Always call a tool first
Small models often answer from memory even when the prompt tells them to search. Set "toolChoice": "required-first-round" in voicethere.llm.json and the first model call of every turn must call a tool, so the model has the result in front of it before it says anything. The calls after that are free again: the model can answer or call another tool. Because the forced call uses up one round, this setting needs tools.maxRounds of 2 or more, and the config is rejected with maxRounds: 1. It is ignored when the agent has no tools. Small talk such as "thanks" also triggers a tool call, so use it for assistants whose answers should always come from a tool, like a docs search. The default is "auto".
Tool timeout
How long one tool call may run, in milliseconds, before the agent goes on without its result.
Fillers
Fillers are short phrases the agent says while the caller waits, so the line does not go quiet. They live under fillers in voicethere.llm.json and cover three situations. A situation with no phrases is off, and with no fillers block at all the agent says nothing extra.
- Thinking (
fillers.thinking): the model has not produced a first sentenceafterMsafter the request started. Default 1500, allowed 300 to 10000. Each phrase is 1 to 60 characters. It plays at most once per turn. - Before a tool call (
fillers.toolCall): plays right when the model asks for a tool, and only if nothing has been said in the turn yet. Each phrase is 1 to 120 characters. It plays at most once per turn. - Long tool call (
fillers.toolWait): a tool round is still runningafterMsafter it started. Default 4000, allowed 1000 to 30000. Each phrase is 1 to 120 characters. It plays at most once per tool round and twice per turn.
Each list holds up to 20 phrases. The agent picks one at random and never uses the same phrase twice in a row. A turn gets at most 3 fillers, and two never play back to back. Fillers are not sent to the model, so they do not appear in its history. fillers.interruptible (default on) decides whether a caller who starts talking cuts a filler off.
The older tools.filler setting still works while fillers.toolCall and fillers.toolWait have no phrases. Once either has phrases, tools.filler is ignored. The LLM tab leaves an existing tools.filler as it is; move its text into Before a tool call.
Speech pacing
speech.coalesceMs (default 150, allowed 0 to 1000) is a window in milliseconds. Sentences of a reply that finish within it are spoken as one utterance, so a reply that arrives all at once has no gap between sentences. With 0, each sentence is spoken on its own.
Spoken assistant text
A spoken turn also reaches the client as text. For each sentence the agent speaks, it sends assistant_delta with spoken: true, and it ends the turn with assistant_done, which carries spoken: true, the text that was spoken and a status (completed, interrupted, cancelled or failed). When the turn answers a transcribed caller utterance, both messages also carry inReplyTo with that utterance id. Group them by turnId. Fillers send nothing. A turn that ends before any sentence was spoken sends no assistant_done.
You do not need to handle these messages yourself. The browser session in @voicethere/client/browser takes an onAgentText callback and calls it with { turnId, text, spoken, final, inReplyTo } every time the visible text of a turn changes. text is the whole turn so far, so you replace the line for that turnId instead of appending. By default a spoken turn is revealed word by word between the speaking start and end events, and it stops where it is when the caller barges in or the turn ends as interrupted, cancelled or failed. Typed turns arrive at once, and the chat_reply that repeats a typed answer is not reported a second time.
Set spokenTextReveal: "immediate" on the session to get each spoken sentence as soon as the agent hands it to speech, without pacing. The default is "playback". onControlMessage still receives every raw message.
Links in replies
Read aloud, a URL is noise ("https colon slash slash"), so from @voicethere/agent0.22.0 the agent takes every URL out of the model's reply before it is spoken. speak, assistant_delta and assistant_done carry the sentence without it. Each https URL of the turn is sent once, on its own, as an assistant_content message:
{
"type": "assistant_content",
"turnId": "…",
"content": { "kind": "link", "url": "https://…", "label": "pricing page" }
}It arrives before that turn's assistant_done and has the same turnId, so a client attaches the link to the reply it belongs to. Voice turns add spoken: true and inReplyTo, like the text messages. A markdown link such as [pricing page](https://…) keeps its label in the spoken sentence and sends it as label. A short lead-in like "Docs:" is dropped together with the link. http:// links are removed from speech and never sent. A bare address such as example.com/pricing is sent as an https:// link too; the host stays in the spoken sentence as the label and the path is left out. Clients should ignore content.kind values they do not know; link is the only kind today.
The links block in voicethere.llm.json controls this:
mode:"extract"(default) or"off", which leaves the reply as the model wrote it, URL included.requireToolSource(defaultfalse): send only URLs that appear in a tool result of the same turn. Turn it on with a docs search tool so the model cannot link to a page it made up. Other URLs are still removed from speech.maxPerTurn: 1 to 10, default 3.
Hooks and tools can send a link of their own with ctx.sendContent({ kind: "link", url, label }). It goes through the same path: https only, a URL already sent in the turn is skipped, and maxPerTurn applies. It returns whether the message was sent and throws on invalid content. Called from onTurnEnd it sends nothing and returns false. The turn summary reports linksSent and linksDropped.
In the browser, @voicethere/client 0.18.0 and later reports each link through the onAgentContent session callback as { turnId, spoken, content, inReplyTo }, and parseAgentContentEvent applies the same checks to a raw control message. Only https links are reported. The embed widget shows each link as its own line in the transcript and opens it in a new tab.
Voice call prompt
The LLM agent template ships a Voice call section at the end of prompt.md. It tells the model that it is on a live phone call: answer right away and never leave the caller in silence, keep sentences short with one idea each, use no lists, markdown or emoji, say numbers and dates the way a person would, and offer what it can do when it cannot help. Keep that section when you rewrite the prompt. The prompt does not ask the model to announce a tool call: the Before a tool call filler covers that moment. Since agent 0.18.2 the line asking for a sentence before each tool call is gone, because the model sometimes said the sentence and then never called the tool. If your own prompt still has it, remove it.
Project service tools
When the project has a service, the agent can use the tools that service exposes over MCP. The default path is /mcp.
External MCP servers
MCP servers run by someone else, reached over https. Authentication is none, a bearer token from an AGENT_ secret, or headers whose values come from AGENT_ secrets.
Allowed and blocked tools
Per tool source, a comma separated list of tool names. When an allow list is set, only those tools reach the model; a block list removes tools from the rest.
Discovered tools
Under each tool source the LLM tab lists the tools that server offers, with a description and an on/off switch per tool. The list comes from the agent: when a session starts, the agent connects to each source and writes the tool names into your project's agent logs. The tab shows the newest list per source from the last 30 days, with the time it was seen. A new project has no list until you publish and start a test chat.
The switches edit the allowed and blocked lists for you. Every tool starts on. Turning one off adds it to the blocked list, and turning it on removes it again. If the source already has an allowed list, turning a tool on adds it there instead, and turning it off removes it. Turning off the last allowed tool would leave an empty allowed list, which means every tool, so the tab clears the allowed list and blocks every tool the server reported instead. Tools the server adds later then show up on. A tool that sits in both lists counts as off. Names in the lists that the server did not report show as not seen, which usually means a typo or a tool the server dropped. Switches change the form like any other edit, so they take effect after Save and Publish.
Discover now on an external MCP server connects from the dashboard and refreshes its list without a session. It uses the saved settings, so save first, and the button stays off while the form has unsaved changes. The address has to be a public https URL, and secrets are read from the AGENT_ secrets the server is configured with. Calls are limited to one per server every 30 seconds. The project service has no button, because the dashboard cannot reach it; its tools appear when a session starts.
Tool call logs
Every tool the agent calls writes one line to your project's agent logs: the tool, where it came from (an MCP server, the project service, a tool you defined, or a hook), how long it took, whether it failed, and the input and output. Find them in Agent logsby setting the Type filter to Tool calls; expand a row to read the input and output. The LLM tab links there. On a session page, the Timeline shows each call as its own row, and the Display menu has a Tool calls switch to hide them. The Transcript view shows the same calls between the caller's turn and the agent's reply. This needs @voicethere/agent 0.17.0 or newer; older sessions have no tool calls to show.
By default the agent redacts the input and output before it logs them. It removes the values of your AGENT_ secrets, values under keys such as password, token, api_key, authorization or cookie (also as a suffix, like accessToken), and personal data in text: email addresses, formatted phone numbers, card numbers that pass the Luhn check, and IBANs. A removed value shows as [redacted] or [redacted:email], [redacted:phone], [redacted:card], [redacted:iban], [redacted:secret]. Input and output are each cut at 2048 characters, and the row says which one was cut.
Set toolCallLogs in voicethere.llm.json to change it: "redacted" (the default), "full" to keep keys and personal data, or "off" to log no tool calls. Secret values are removed in every mode, including "full". Tool calls follow your agent log retention: once a line expires, the conversation view shows the transcript without it. The conversation API returns tool rows only with ?include=tool_calls. The logs belong to your project; VoiceThere does not copy tool input or output into its own logs.
Copy the loop into your code
The conversation loop normally comes from the @voicethere/agent package. Copying it puts the loop in llm/loop.ts in your project, so you can change how a turn works. Package updates then no longer change the loop, while the building blocks keep updating. To undo it, restore the previous code revision.
Publish
Save only changes your code. Publishing compiles it, uploads the build and rolls it out, with the same buttons as in the Code tab.