rich-application-94483
05/19/2026, 6:58 PMtall-airline-16756
05/19/2026, 6:58 PMtall-airline-16756
05/19/2026, 6:58 PMsilent: true in the Agent Builder so the agent does not generate a follow-up response specifically about the tool result. This avoids an extra visible interaction and keeps the agent focused on the conversation. [Function tool definition; Agent Builder]
• Have the LLM include the next user message in its tool call intent (embedding the reply in the tool payload).
◦ The model still initiates a tool call, but you can design the tool’s input to include the text the agent should say next. The tool executes and records data; your system then returns that pre-composed reply as the agent response. Note: LiveKit still performs the tool-execute → response pattern, so this does not change that there are two internal steps, but it saves the second model generation.
• Optimize for latency and cost:
◦ Use the smallest/lower-cost model capable of the decision to call the tool, or cache model decisions where possible, so the extra step is cheaper/faster. Also make the tool execution extremely fast (local DB write, in-memory queue) so the overall latency is minimal.
If you’d like, tell me which agent builder you’re using (HTTP client tool, custom tool, or function tool) and whether you prefer the reply to be pre-composed by the LLM or generated after execution — I’ll provide the exact configuration or example payload you can use.
Sources: Sequential Pipeline Architecture for Voice Agents | LiveKit | The ReAct Pattern for Voice Agents and How AI Agents Think, Act, and Respon... | Large language models (LLM) overview | LiveKit Documentation | Function tool definition | LiveKit Documentation | Agent Builder | LiveKit Documentation, with interactive elementsrich-application-94483
05/19/2026, 7:04 PMtall-airline-16756
05/19/2026, 7:04 PMfunction_tool that records context silently (so the agent doesn’t need a second LLM call to acknowledge the tool).
Short answer: return None (or omit a return) from the function_tool to make it silent. Below is a minimal example showing two options:
• Silent tool that only stores context (no LLM follow-up).
• Tool that also returns a pre-composed agent reply (the tool runs, then you can return that text as the agent response).
from livekit.agents import Agent, function_tool, RunContext
class MyAgent(Agent):
# Option A: Silent recorder — records context and returns None (no LLM reply)
@function_tool()
async def record_context_silent(self, context: RunContext, key: str, value: str) -> None:
"""Store context metadata silently; agent will NOT generate a follow-up reply."""
# example: store in session metadata or chat context
context.session.chat_ctx.set_metadata(key, value)
# returning None (or not returning) makes this tool silent
# Option B: Recorder that also supplies the next reply text
@function_tool()
async def record_context_with_reply(self, context: RunContext, key: str, value: str) -> dict:
"""Store context and provide the pre-composed agent reply."""
context.session.chat_ctx.set_metadata(key, value)
# Compose the reply here so your system can emit it without a second model call.
# Return a structure the agent runner can use; e.g.:
return {"agent_reply": f"Thanks — I've saved your {key}."}
Notes and recommendations:
• Option A: Best when you want the tool to run silently and let the agent continue without generating a tool-result-driven reply.
• Option B: LiveKit still calls the tool then generates a reply step, but by returning the pre-composed reply from the tool you can avoid invoking the LLM again to craft that message (your runner must be wired to use the returned agent_reply as the outgoing message).
• Make the tool execution fast (in-memory or quick DB write) to keep overall latency low.
If you want, tell me which pattern you prefer (A or B) and I’ll provide a concrete example of wiring the agent runner to use the tool-returned reply in Option B.
Sources: Function tool definition | LiveKit Documentation | Agent Builder | LiveKit Documentation | Collect structured data with LiveKit Agents | LiveKit | The Handoff Pattern for Voice Agents That Replaces IVR Menus | LiveKit, with interactive elementsrich-application-94483
05/19/2026, 7:08 PMStopResponse helper for and could it be helpful here?tall-airline-16756
05/19/2026, 7:09 PMStopResponse inside a tool, hook, or pipeline node tells the runner to suppress the agent’s follow-up response. Use it when an action should have no verbal acknowledgement.
How it helps your case
• It will not merge tool execution and LLM generation into one call. LiveKit still sequences tool execution and response generation.
• It is useful to avoid an extra user-facing reply after your context-recording tool runs — similar effect to a silent tool.
Python example
from livekit.agents import function_tool, StopResponse, RunContext
class MyAgent:
@function_tool()
async def record_context_and_stop(self, context: RunContext, key: str, value: str) -> None:
# store context
context.session.chat_ctx.set_metadata(key, value)
# prevent the agent from generating a reply
raise StopResponse()
When to use which approach
• Use a silent function_tool (return None) when you simply want the tool to run without causing any reply.
• Use StopResponse when you need conditional suppression inside hooks or more complex logic.
• If you want the agent to say something but avoid a second LLM call, have the tool return a pre-composed reply (and wire your runner to use it) — StopResponse does not create or return such text.
Tell me which option you prefer (silent tool, StopResponse, or tool-returned reply) and I’ll give the exact wiring example for the Python runner.
Sources: Pipeline nodes and hooks | LiveKit Documentation | Agent Builder | LiveKit Documentation, with interactive elementsrich-application-94483
05/19/2026, 7:10 PMStopResponse and session.say together to combine the tool and say functionality into a single pass?tall-airline-16756
05/19/2026, 7:11 PMfunction_tool with session.say() and StopResponse to record context and speak a pre-defined reply without an extra LLM-driven response. That achieves the behavior you described: the LLM invokes the tool, the tool records state and triggers speech, and no follow-up generation is produced.
Below is a compact Python pattern you can use.
1) function tool that records context and calls session.say()
from livekit.agents import Agent, function_tool, RunContext
from livekit.agents.llm import StopResponse
class MyAgent(Agent):
@function_tool()
async def record_and_speak(self, context: RunContext, next_message: str) -> None:
# 1) persist context (session metadata / DB / state)
context.session.chat_ctx.set_metadata("last_note", next_message)
# 2) schedule outgoing speech immediately (bypasses LLM reply)
# `allow_interruptions` optional depending on your UX needs
await context.session.say(next_message, allow_interruptions=True)
# 3) ensure tool is silent (return None) — the runner won't surface a tool result
return None
2) lifecycle hook to suppress the LLM reply after the tool runs
async def on_user_turn_completed(self, ctx: RunContext, new_message) -> None:
# Detect that your tool ran and scheduled speech (use a flag or inspect session/chat_ctx)
last_note = ctx.session.chat_ctx.get_metadata("last_note")
if last_note:
# prevent the agent from generating a follow-up LLM response
raise StopResponse()
Notes and recommendations
• The model still makes a tool-choice call and the tool executes; you are not merging two model invocations into a single raw LLM API call. Instead, you avoid the second model-generated reply by having the tool emit speech directly and stopping the agent from generating text.
• Use a lightweight, deterministic next_message value (or precompose it) so the tool can call session.say() without needing further LLM reasoning.
• Use a clear flag (session metadata or an agent attribute) to signal the lifecycle hook that speech was scheduled, then raise StopResponse() to suppress the follow-up.
• Ensure the tool action (DB write, metadata update, say) is fast to keep latency low.
If you want, I can provide a complete runnable example showing how to set the metadata flag inside the tool, clear it after use, and how to wire this into your agent runner. Which runner/version of the Python SDK are you using?
Sources: Function tool definition | LiveKit Documentation | Audio customization | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation | Agent speech and audio | LiveKit Documentation | The ReAct Pattern for Voice Agents and How AI Agents Think, Act, and Respon..., with interactive elementsrich-application-94483
05/19/2026, 7:14 PMlast_note back such that the LLM will respond when the user talks again?tall-airline-16756
05/19/2026, 7:14 PMlast_note) once the tool-scheduled speech has been delivered, so subsequent user turns are treated normally. A simple, reliable pattern:
• Set the flag in the tool when you call session.say() (you already do this).
• In the lifecycle hook that runs after the agent’s outgoing speech finishes or at the start of the next user turn, clear that flag.
• Only raise StopResponse() if the flag is present. When cleared, the hook will not raise and the LLM will generate a reply as usual.
Minimal example (continuing prior code):
# inside the function tool
await context.session.say(next_message, allow_interruptions=True)
context.session.chat_ctx.set_metadata("last_note", "pending")
# lifecycle hook to suppress reply immediately after tool
async def on_user_turn_completed(self, ctx: RunContext, new_message) -> None:
flag = ctx.session.chat_ctx.get_metadata("last_note")
if flag == "pending":
# clear the flag so next user turn will allow LLM responses
ctx.session.chat_ctx.set_metadata("last_note", None)
raise StopResponse()
# optional: if you have an event that fires after speech finished, clear flag there instead
async def on_speech_finished(self, ctx: RunContext) -> None:
ctx.session.chat_ctx.set_metadata("last_note", None)
Recommendations
• Use a deterministic value (e.g., "pending") and clear to None or delete the key.
• Clear the flag as soon as the speak action is complete (either in a speech-finished hook or the first lifecycle hook after the speak call) to avoid accidentally suppressing the next user turn.
• If interruptions are allowed, account for cases where the user speaks during the agent’s speech — you may want to clear the flag immediately on interruption so the LLM can respond.
If you tell me which lifecycle events your runner exposes (e.g., on_speech_finished, on_user_turn_started, or only on_user_turn_completed), I can tailor the exact hook placement.
, with interactive elements