Why did this fail?? ```Traceback (most recent cal...
# ask-ai
p
Why did this fail??
Copy code
Traceback (most recent call last):
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/utils/log.py", line 16, in async_fn_logs
    return await fn(*args, **kwargs)
  File "/app/.venv/lib/python3.10/site-packages/opentelemetry/util/_decorator.py", line 71, in async_wrapper
    return await func(*args, **kwargs)  # type: ignore
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/voice/generation.py", line 131, in _llm_inference_task
    async for chunk in llm_node:
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/voice/agent.py", line 418, in llm_node
    async for chunk in stream:
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/llm/llm.py", line 354, in __anext__
    raise exc  # noqa: B904
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/llm/llm.py", line 181, in _traceable_main_task
    await self._main_task()
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/llm/llm.py", line 212, in _main_task
    raise APIConnectionError(
livekit.agents._exceptions.APIConnectionError: failed to generate LLM completion after 4 attempts (body=None, retryable=True)
t
Thanks for reaching out, Mario. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
p
Copy code
Error in _llm_inference_task
Traceback (most recent call last):
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/inference/llm.py", line 276, in _run
    chat_ctx, _ = self._chat_ctx.to_provider_format(format=self._provider_fmt)
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/llm/chat_context.py", line 493, in to_provider_format
    return _provider_format.openai.to_chat_ctx(self, **kwargs)
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/llm/_provider_format/openai.py", line 14, in to_chat_ctx
    item_groups = group_tool_calls(chat_ctx)
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/llm/_provider_format/utils.py", line 41, in group_tool_calls
    item_groups[group_id].add(item)
  File "/app/.venv/lib/python3.10/site-packages/livekit/agents/llm/_provider_format/utils.py", line 76, in add
    assert self.message is None, "only one message is allowed in a group"
AssertionError: only one message is allowed in a group
t
Thanks — I see the traceback and you’re asking why it failed. The error at the end, APIConnectionError: failed to generate LLM completion after 4 attempts (body=None, retryable=True) means the LiveKit Agents framework tried to call your configured LLM provider but could not establish or maintain a successful connection. It retried up to 4 times (the error is marked retryable) and all attempts failed. What commonly causes this • Network/connectivity issues between your app and the LLM provider (timeouts, dropped connections). • Outbound traffic blocked by a firewall, proxy, or deep packet inspection (e.g., ZScaler) that interferes with HTTPS to the provider. • The LLM provider is temporarily down or rate-limiting / throttling your requests. • Invalid or missing provider API key / auth causing the provider to refuse the request at connection time. • Very short request timeouts in your environment causing the client to give up before the provider responds. Quick checks to narrow root cause 1. Verify provider credentials are set and valid (e.g.,
OPENAI_API_KEY
,
GROQ_API_KEY
). 2. From the same host/container where the agent runs, run a quick call to the LLM endpoint (curl or Postman) to confirm connectivity and auth. If that call fails, the issue is network/auth, not LiveKit. 3. Check your network path: ◦ If you’re behind a corporate proxy or DPI (ZScaler, etc.), ensure the provider endpoints are whitelisted and that HTTPS inspection is not breaking TLS. ◦ Confirm outbound port 443 is allowed to the provider domain(s). 1. Check the LLM provider status page for outages or rate-limiting. 2. Look for provider error responses or logs (if any): the traceback shows
body=None
, so the client never got a valid response body back — that points to connection/timeouts rather than an application-level error. If the curl/short test fails, here are focused next steps • If it’s a proxy/DPI issue: bypass or create an allow rule for the LLM provider domains (and LiveKit TURN domains if used). See guidance for ZScaler/inspection bypass if applicable. • If it’s auth: re-check that the API key is present in the runtime environment and not expired. • If it’s transient provider outage: retry later and monitor provider status. You can also increase request timeout or retry window if intermittent network hiccups are expected. If you’d like, tell me: • Which LLM provider you’re using (OpenAI, Groq, etc.). • Whether the agent runs inside a corporate network or cloud container. • The result of a simple connectivity test from the same host (curl command and response or error). With those details I can suggest the next targeted step to resolve this. Sources: Configure ZScaler proxy for LiveKit connections | LiveKit | examples/survey | livekit/livekit_composite | DeepWiki | examples/avatar_agents | livekit/livekit_composite | DeepWiki | Groq LLM plugin guide | LiveKit Documentation
Thanks — I see two separate errors in your traces. I’ll explain what each one means, why it happens, and give clear steps to troubleshoot and fix them. 1) APIConnectionError: failed to generate LLM completion after 4 attempts (body=None, retryable=True) What this means • The agent tried to call your configured LLM provider but could not successfully complete the request after 4 retry attempts. • The error is connection-level (not a content error) and marked retryable, so retries were attempted but all failed. Likely causes to check • Network/connectivity issues between your service and the LLM provider (outbound HTTPS blocked, corporate proxy, firewall, or DPI like ZScaler). • The LLM provider endpoint is unavailable or temporarily down (provider outage or throttling). • Invalid, missing, or expired API credentials for the LLM provider. • Very long requests or timeouts causing the provider connection to fail. Actionable troubleshooting steps 1. Verify credentials and config ◦ Confirm the correct API key/env var is set (e.g.,
OPENAI_API_KEY
or provider equivalent) and not expired. 1. Test direct connectivity ◦ From the same environment where the agent runs, run a simple curl or Postman call to the provider endpoint (for example, `https://api.openai.com/...`) to confirm you can reach and authenticate the provider outside LiveKit. 1. Inspect network path ◦ Ensure outbound HTTPS (port 443) to the provider domain is allowed. ◦ If you’re behind corporate network controls, have the provider domain whitelisted or exempted from DPI. 1. Check provider status & rate limits ◦ Look at the provider’s status page and your account usage/limits to rule out outages or throttling. 1. Retry/timeout adjustments ◦ If connectivity is flaky, try again later. If failures are frequent, consider increasing timeouts or retry windows if configurable. 1. Collect logs ◦ Capture request attempts, timestamps, and any lower-level socket/timeouts — useful if you need to dig deeper. 2) AssertionError: only one message is allowed in a group (during chat context → provider format) What this means • When converting the internal ChatContext to an OpenAI-style request, the formatter groups items by tool_call_id and expects at most one assistant message per group. • A group contains more than one assistant/text message, which triggers the assertion. Why this happens • Your code or a node produced multiple assistant messages tied to the same tool call/grouping. Common situations: ◦ Calling multiple
say()
or
generate_reply()
(or equivalent) after a tool returns, within the same logical turn. ◦ Manually injecting multiple assistant messages into the ChatContext after a tool call. ◦ A tool or custom node emitting multiple textual outputs that get grouped under the same
tool_call_id
. How to fix it (concrete guidance) • Ensure one final assistant message per tool-call group: ◦ After a tool returns, only produce a single assistant reply. Avoid chaining multiple
say()
calls after the tool completes. • Prefer returning structured tool results and letting the LLM generate the reply: ◦ Return the result from the tool and call
session.generate_reply()
once (or let the standard flow generate the assistant message). • If you need interim messages, emit them before the tool returns (so they’re not grouped with the tool's result), or ensure interim messages are canceled/replaced and not left as multiple messages in the same group. • Review any custom
llm_node()
or overrides: ◦ Confirm they do not emit multiple `ChatChunk`/assistant messages without separating them into distinct logical turns. • Audit code paths that manipulate `chat_ctx`: ◦ Avoid calling
chat_ctx.add_message()
multiple times for assistant messages after a tool result. Minimal examples • Bad (multiple messages after tool):
Copy code
# Do not do this after a tool finishes
await ctx.session.say("Part 1")
await ctx.session.say("Part 2")
• Good (single reply or return structured data):
Copy code
# Return structured result and let the LLM generate the reply
return {"status": "ok", "summary": "..."}
# Or if you must speak, call generate_reply once:
await ctx.session.generate_reply(instructions="Give a short status message.")
Next steps I can help with • If you paste the code for the tool or node that runs around the failing tool-call (the function that runs the tool and any immediate
say()
/
generate_reply()
usage), I can point out the exact line causing multiple messages. • If you want, provide a short excerpt of how you call the LLM provider or the environment/network topology and I can help narrow down the APIConnectionError (for example, a curl result or provider response/timestamp). If you share the relevant snippets (tool function and where it returns/emits messages) I’ll review and give a specific change to make. Sources: Tool definition and use | LiveKit Documentation | Pipeline nodes and hooks | LiveKit Documentation
p
if an agent doesn't have any tool calls, only a generate_reply on_enter, and then, a transition to another agent, with update_agent. Why would that happen?
t
Thanks — this is a great, specific question. You described an agent that does no tool calls, calls
generate_reply
in
on_enter
, then immediately transitions to another agent with
update_agent
. Here's why that can still trigger the "only one message is allowed in a group" assertion and what to do about it. Why this can happen •
generate_reply
starts an asynchronous process that creates an assistant message in the chat context. •
update_agent
switches the active agent immediately. If the
generate_reply
-initiated message is still in-flight (not fully committed/finalized) when the transition happens, the system can end up with two assistant messages associated with the same logical group/turn: the unfinished message from the previous agent and the new message from the next agent. • The OpenAI-style formatter groups items by tool_call_id (or logical turn). It expects at most one assistant message per group. Two messages in the same group cause the assertion. How to prevent it (practical fixes) • Wait for the speech handle to finish or cancel it before calling
update_agent
. ◦ If
generate_reply
returns a handle or task, await its completion (or call its cancel method) before transitioning. • Finalize one message before creating the next. ◦ Ensure the previous agent’s reply is completed (or explicitly finalized) so it won’t remain as a pending assistant message. • Move the transition so it happens after LLM reply generation completes. ◦ If the transition must be immediate, consider returning structured state from the current agent and let the next agent generate the user-facing message. • Avoid calling
generate_reply
and
update_agent
concurrently from the same turn. ◦ If interim messages are needed, emit them before
generate_reply
or use a single
generate_reply
and then transition. • Use the agent handoff pattern designed for safe transitions (tool return + next agent) if available in your flow. What I can look at for a precise fix • A short snippet showing your
on_enter
(the
generate_reply
call) and the
update_agent
call. • Whether you get a handle/backchannel from
generate_reply
and how you use it (do you await it, cancel it, ignore it). • Whether the transition is triggered inside the same task or from another async callback. If you paste the minimal code around
generate_reply
and
update_agent
, I’ll point out exactly where to await/cancel or how to restructure the flow to avoid the assertion. Sources: livekit-plugins/livekit-plugins-aws/livekit/plugins/aws/experimental/realti... | Agent speech and audio | LiveKit Documentation
p
I await generate_reply, and right after it I call update_agent
t
Thanks — that helps. You awaiting
generate_reply
first is the right instinct, but this can still produce the "only one message is allowed in a group" assertion in some timing cases. Here’s why that can happen and concrete steps to avoid it. Why it can still fail • Awaiting
generate_reply
ensures the call returns, but it may not guarantee the assistant message is fully committed/finalized in the shared ChatContext before you call
update_agent
. • Internals: the reply generation may complete in multiple async steps (streaming, chunk finalization, context write), and the final assistant message can remain in a pending/in-flight state briefly after the await returns. • When you call
update_agent
immediately, the transition can cause the system to treat the new agent’s outgoing message as part of the same logical group/turn as the previous agent’s in-flight message. That yields two assistant messages in the same group and triggers the assertion. Practical ways to fix it • Ensure the reply is fully finalized before switching agents: ◦ If
generate_reply
returns a handle or object with a completion/finalization indicator, explicitly wait for that finalization (not just the initial await). ◦ If you get a stream/handle, await its
.done()
or similar final event, or poll until the context shows the assistant message as final. • Cancel or explicitly finalize in-flight speech before updating: ◦ If you don’t need the previous reply to persist, cancel the in-flight handle so it won’t leave a pending assistant message. • Insert a short, explicit synchronization point after `generate_reply`: ◦ Example: await a small confirmation promise that the message has been written to chat context, or check chat context state until the assistant message is committed. • Move the transition responsibility out of the same task: ◦ Instead of calling
update_agent
immediately, return a structured result from the current agent and have the agent manager perform the transition after it verifies the message is finalized. • Use a single source of truth for transitions: ◦ Trigger
update_agent
only from an orchestrator that knows when messages are committed (helps avoid race conditions). Concrete checklist • Do you get any stream/handle from
generate_reply
? If yes: ◦ Await the finalization event on that handle, not just the initial return. • If you cannot observe finalization: ◦ Add a short retry loop checking chat context until the assistant message is present and marked final, then call
update_agent
. • If you want to abort previous message: ◦ Explicitly cancel the reply handle before switching agents. If you share the minimal snippets for: • the
generate_reply
call and what it returns, and • the immediate
update_agent
call, I’ll point to the exact line to await/cancel or show a compact pattern to guarantee no in-flight assistant message remains before the transition.
p
show me a sample code in python
t
Thanks — below is a concise Python example that shows the recommended pattern: await the
SpeechHandle
returned by
generate_reply()
to ensure the agent's reply is fully processed before calling
update_agent()
. This prevents the "only one message is allowed in a group" assertion by avoiding an in-flight assistant message at the moment of the agent transition. Notes: • The example uses the
SpeechHandle
returned by
generate_reply()
and awaits it (this ensures speech generation/commit finishes). • If you want to discard the reply instead of waiting, call
handle.cancel()
before
update_agent()
. Example:
Copy code
from livekit.agents import Agent, AgentSession

class FirstAgent(Agent):
    async def on_enter(self):
        # Start reply generation and get the SpeechHandle
        handle = await self.session.generate_reply(instructions="Hello — I'll hand off after this reply.")
        # `generate_reply` may return a SpeechHandle; awaiting it ensures completion.
        # If generate_reply returns a handle without awaiting internally, do:
        # handle = self.session.generate_reply(...)
        # await handle

        # Now safe to transition — the assistant message is finalized
        self.session.update_agent(SecondAgent())

class SecondAgent(Agent):
    async def on_enter(self):
        await self.session.generate_reply(instructions="Hello from the second agent!")

async def run_session(room):
    session = AgentSession(
        # configure llm, tts, stt, etc.
    )
    first = FirstAgent()
    await session.start(agent=first, room=room)
    # session runs until closed

# If you need to cancel instead of waiting:
# handle = session.generate_reply(instructions="...")
# handle.cancel()
# session.update_agent(SecondAgent())
If your
generate_reply()
returns a handle object rather than completing fully, make sure you: • Await the handle (e.g.,
await handle
) to wait for finalization, or • If you do not want the reply to persist, call
handle.cancel()
before
update_agent()
. If you share the exact return value/type that your
generate_reply()
call gives (a direct awaitable vs. a
SpeechHandle
object), I can adapt the snippet to precisely match your environment. Sources: Agent speech and audio | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | agents/src/voice/agent_activity.ts | livekit-agents/livekit/agents/voice/run_result.py