proud-lawyer-39815
02/12/2026, 9:21 PMTraceback (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)tall-airline-16756
02/12/2026, 9:22 PMproud-lawyer-39815
02/12/2026, 9:22 PMError 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 grouptall-airline-16756
02/12/2026, 9:22 PMOPENAI_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 Documentationtall-airline-16756
02/12/2026, 9:22 PMOPENAI_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):
# 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):
# 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 Documentationproud-lawyer-39815
02/12/2026, 9:24 PMtall-airline-16756
02/12/2026, 9:25 PMgenerate_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 Documentationproud-lawyer-39815
02/12/2026, 9:30 PMtall-airline-16756
02/12/2026, 9:30 PMgenerate_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.proud-lawyer-39815
02/12/2026, 9:31 PMtall-airline-16756
02/12/2026, 9:33 PMSpeechHandle 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:
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