famous-translator-19267
03/01/2026, 11:56 PMtall-airline-16756
03/01/2026, 11:56 PMtall-airline-16756
03/01/2026, 11:57 PM@function_tool()
async def lookup_weather(context: RunContext, location: str) -> str:
result = await external_api_call(location)
# Return None to suppress an intermediate message
return None
1. Node.js (tool execute)
const lookupWeather = llm.tool({
description: 'Get weather for a location',
parameters: z.object({ location: z.string() }),
execute: async ({ location }, { ctx }) => {
const result = await externalApiCall(location);
// Return null to suppress immediate message
return null;
},
});
Why this works
• Returning `None`/`null` signals that the tool produced no conversational output. The agent will still have the tool’s effects available in its context (so it can use the tool result internally) but won’t generate a visible/intermediate turn. The model then generates a single final response afterwards.
If you want, I can help with:
• Reviewing one of your tool implementations to show exactly where to return `None`/`null`.
• Ensuring the agent has access to the tool result internally so the final response can include the needed information.
Would you like me to review a code snippet from your agent so I can point out the exact change?
Sources: Tool definition and use | LiveKit Documentationfamous-translator-19267
03/01/2026, 11:58 PMtall-airline-16756
03/01/2026, 11:58 PMcontext.state['weather'] = result or ctx.set('weather', result)).
c. Tool returns None to suppress intermediate reply.
d. Agent reads context.state['weather'] when generating the final reply.
• Return a machine-readable structure but mark it non-conversational:
◦ Some systems let tools return structured data (JSON) that is treated as non-visible. The agent reads that structured payload and then generates a natural-language response. The tool’s direct text output is still suppressed.
Example patterns
1) Python-style (conceptual)
@function_tool()
async def lookup_weather(context: RunContext, location: str) -> str:
result = await external_api_call(location) # e.g., {"temp":72, "cond":"sunny"}
context.set("weather_result", result) # store result for agent use
return None # suppress intermediate message
2) Node.js-style (conceptual)
const lookupWeather = llm.tool({
execute: async ({ location }, { ctx }) => {
const result = await externalApiCall(location); // structured result
ctx.set('weather', result); // save to agent context
return null; // no intermediate output
},
});
What to check in your implementation
• Verify your tool has access to a context or state object where it can save the fetched data.
• Confirm the agent’s final-generation step reads that same context key and includes the data in its reply.
• If your environment supports structured tool returns separate from conversational output, use that mechanism to pass the result to the agent without producing a visible intermediate message.
If you’d like, share a short snippet of your current tool and agent code (how you call the tool and where you generate the final response). I can point out exactly where to store the result and how to read it so you get a single, final reply that includes the weather.famous-translator-19267
03/02/2026, 12:00 AMtall-airline-16756
03/02/2026, 12:01 AMcontext.set('weather', result) or ctx.set('weather', result)). When the agent composes its final response, it reads that stored data and includes it in one combined reply.
What to verify in your setup
• The tool stores its results in a context/state object the agent can read.
• The tool returns `None`/`null` (not a string).
• The agent’s generation step reads the context key and uses it to build the response.
If you want, I can:
• Review a short code snippet to point out exactly where to store the result and where to read it, or
• Provide a minimal end-to-end example that shows the tool, storing the result, and the agent producing one final response.
Which would you prefer?famous-translator-19267
03/02/2026, 12:04 AMtall-airline-16756
03/02/2026, 12:05 AMNone so it produces no intermediate conversational message.
• Save the actual tool result into the agent’s shared context/state (so the final generator can read it).
• Ensure the agent’s final reply generation reads that stored state and composes the single visible response.
Concrete changes for your class
1) Store tool results in a context field
• Add a place to keep tool results in your agent (you already have `self.resumen`; we can reuse or add a self.tool_results dict).
Example addition in init:
self.tool_results: dict[str, Any] = {}
2) Modify each tool to save its structured result and return None
• Replace the current return strings with storing the string into self.tool_results[...] and return None.
Example for `si_paga`:
@function_tool()
async def si_paga(self):
# store the message for later use by the agent's final reply
self.tool_results['si_paga'] = "Recuerde canales de pago. Invite a validar el valor exacto de la cuota."
return None
Apply the same pattern to the other tools:
• ya_pago -> store "Mencione que entiende..." in self.tool_results['ya_pago'] and return None
• paga_fuera_de_fecha -> store the message and return None
• definitivamente_no_paga -> store the message and return None
• no_identifica_intencion -> store the message and return None
3) Ensure the agent reads the stored result when composing the final reply
• Wherever you call self.session.generate_reply to produce the agent’s visible response, include logic to read the appropriate entry from self.tool_results and feed it into the final instructions or let the model use it from context.
Example pattern (conceptual):
# After the tool call completes, decide which tool key was used (your decision logic)
tool_key = 'si_paga' # set based on tool selection
tool_message = self.tool_results.get(tool_key)
# Provide the final instructions including the tool_message
await self.session.generate_reply(
instructions=f"{tool_message} Mantenga respuestas breves, una o dos oraciones.",
allow_interruptions=False
)
Notes and tips
• Returning None prevents the tool from sending any visible text. The agent still has programmatic access to the data because you stored it in self.tool_results (or self.resumen).
• Make sure your decision logic (how the agent chooses which tool to call) still runs normally. After the tool runs silently, the agent should continue to its final generation step where it reads stored data.
• If your framework injects RunContext or ctx into tools, you can also use ctx.set(...) rather than saving to self, but both approaches work if the generation step reads the same storage.
• Keep tool outputs structured where useful (e.g., { "intent": "si_paga", "msg": "..." }) — this makes it easier to compose the final reply programmatically.
Would you like me to:
• Patch the specific tool methods in your snippet and return a complete, edited class version?
• Or show an end-to-end example where the agent calls a tool, the tool stores the result, and the final generate_reply composes the single voice response?
Tell me which you prefer and I’ll produce the exact code change.famous-translator-19267
03/02/2026, 12:08 AMtall-airline-16756
03/02/2026, 12:08 AMself.session or the voice pipeline)?
Based on your answers I’ll provide one of these concrete solutions:
• A small wrapper that filters/suppresses intermediate model messages (if you can intercept outgoing messages).
• A change to the planning call sequence so the agent requests tools first and then yields a single final reply (if you can control the LLM loop).
• A post-processing rule to detect and drop short “I will check” style messages while preserving tool return strings (if you cannot change the planner).
If you prefer, paste the code where the agent sends model-generated text to the call (or the agent planning loop). I’ll point to the exact place to intercept and suppress the pre-tool utterance so you get only one spoken reply while preserving the tool returns.famous-translator-19267
03/02/2026, 12:10 AMtall-airline-16756
03/02/2026, 12:11 AMon_user_turn_completed o un hook similar en tu pipeline— para detectar intención y decidir la respuesta final sin dejar que el LLM emita su turno intermedio.
• Esa estrategia evita tocar las funciones @function_tool() (siguen retornando sus strings) y evita que el mensaje pre-tool llegue al tts/session.
Patrón recomendado (concepto + dónde integrarlo) 1) Implementa un handler tipo on_user_turn_completed(self, turn_ctx, new_message) en tu agente (o el hook equivalente que provee tu runtime). 2) En ese handler, analiza el texto del usuario con reglas simples (o una clasificación ligera) para decidir si corresponde llamar a una de tus herramientas. 3) Si detectas intención clara que mapea a una herramienta, ejecuta la lógica necesaria para la herramienta y, en lugar de permitir la respuesta intermedia del LLM, llama directamente a self.session.generate_reply(...) con la respuesta final que quieres que se pronuncie. Luego evita que el flujo normal genere un mensaje extra. 4) Si no hay intención clara, delega al flujo normal para que el LLM ejecute su planning y genere la respuesta.
Ejemplo conceptual (adáptalo a tu código)
async def on_user_turn_completed(self, turn_ctx, new_message):
user_text = new_message.text_content().lower()
# reglas simples para intentos que mapean a tus @function_tool()
if "si voy a pagar" in user_text or "si voy a pagar" in user_text:
# opcional: llamar a tu herramienta para efectos de registro/validación
# await self.si_paga()
await self.session.generate_reply(instructions="Recuerde canales de pago. Invite a validar el valor exacto de la cuota.", allow_interruptions=False)
return # evita el flujo normal (y el mensaje pre-tool)
if "ya pagué" in user_text or "ya pague" in user_text:
# await self.ya_pago()
await self.session.generate_reply(instructions="Mencione que entiende que ya realizó el pago. Diga que vamos a validarlo en el sistema.", allow_interruptions=False)
return
# ... más reglas para paga_fuera_de_fecha, definitivamente_no_paga, no_identifica_intencion
# Si no coincide ninguna regla, deja que el flujo normal procese y genere respuesta
return await super().on_user_turn_completed(turn_ctx, new_message)
Puntos clave y recomendaciones
• Ventaja: No cambias las herramientas; evitas la reproducción del mensaje pre-tool interceptándolo antes de que sea enviado por el LLM/tts. Tendrás un solo turno hablado por el agente.
• Limitación: Esta solución usa reglas deterministas en el hook. Si la intención es más compleja, puedes:
◦ Usar una llamada ligera a un LLM de clasificación dentro del hook para decidir la intención (cuidado con latencia).
◦ O inspeccionar el primer output del LLM por patrón y descartarlo si parece un "planning" (por ejemplo: frases como "voy a consultar" o "déjeme revisar"); pero eso implica interceptar y filtrar mensajes salientes.
• Si tu runtime no expone on_user_turn_completed exactamente con ese nombre, busca un hook equivalente en la capa de pipeline donde puedas interceptar la respuesta antes del TTS (por ejemplo llm_node -> interceptar su salida antes de tts_node).
Siguiente paso que puedo hacer por ti
• Si quieres, adapto el ejemplo directamente dentro de tu clase AgenteObligacionPreventiva (inserto el método con las reglas y te muestro exactamente dónde colocarlo).
• O si prefieres, pega el punto del código donde recibes mensajes del usuario y reproducen la respuesta (el loop o callback que llama a self.session.generate_reply) y te indico la inserción exacta para interceptar.
¿Quieres que integre el método de ejemplo dentro de tu clase y te devuelva la versión parcheada?
Sources: Testing and evaluation | LiveKit Documentation | Tool definition and use | LiveKit Documentation | Agent speech and audio | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation