boundless-computer-27062
03/26/2026, 9:16 AMAgentHandoff within a function tool while preserving the session's dialogue context. The issue is that the context passed to the new agent does not include the completion of the current function call. Consequently, the next agent enters a loop, repeatedly calling the same function because it lacks the previous tool output.tall-airline-16756
03/26/2026, 9:16 AMtall-airline-16756
03/26/2026, 9:16 AMself.chat_ctx.copy() when constructing the new agent. For example:
return TechnicalSupportAgent(chat_ctx=self.chat_ctx.copy(exclude_instructions=True))
This ensures the new agent sees the function call and its output and won’t repeat the same tool call.
Why this works
• The conversation history records both the LLM’s function call and the tool’s return value as successive turns. If you hand off with a fresh/empty chat context, the new agent won’t see that the function completed and so may re-invoke it.
• Copying the prior chat_ctx preserves those entries so the new agent can reason over them.
Quick question to confirm and help further
• Are you constructing the new agent inside the function tool implementation (i.e., returning a new agent instance from the function), or are you using some orchestration layer to perform the handoff? If you can share the small snippet that triggers the handoff (the agent constructor call), I can point out exactly where to add chat_ctx.copy() or suggest an alternate pattern if your setup differs.
If you prefer, paste a short excerpt of the handoff code (no secrets) and I’ll show the exact change.
Sources: Agents and handoffs | LiveKit Documentation, with interactive elementsboundless-computer-27062
03/26/2026, 9:21 AMchat_ctx.copy() inside function tool then till that time we did not get the function tool completion. aso when we create new agent with that chat_ctx.copy() then next agent when we return TechnicalSupportAgent its chat_ctx wont have tool call completiontall-airline-16756
03/26/2026, 9:22 AMchat_ctx before the function finishes will not include the tool’s return value, so the new agent will miss that completion and may re-call the same tool.
How to ensure the new agent sees the function completion
• The key is to hand off only after the function call has completed and its output has been appended to the conversation history. That means creating (or returning) the next agent after you record the tool output into chat_ctx (or after the runtime has appended the tool output to session.history), then passing a copy of that updated context into the new agent.
Two practical approaches (choose the one that matches your architecture)
1) Return the new agent from the function tool only after appending the tool output to the current chat context
• Flow:
a. Function tool runs and produces a result.
b. Append the tool result as the tool’s return value into self.chat_ctx (or otherwise ensure session.history is updated).
c. Then create the new agent with chat_ctx=self.chat_ctx.copy() and return it.
• This guarantees the new agent’s chat_ctx includes both the LLM function call and the tool output.
2) Let the runtime append the tool output, then perform handoff in the orchestration layer
• Flow:
a. Function returns result to the runtime (normal function-tool return).
b. Runtime appends the tool output to session.history / chat_ctx.
c. Orchestration layer (outside the function) detects your handoff signal and constructs the new agent using chat_ctx=session.chat_ctx.copy() (or equivalent).
• This is usually cleaner when the function tool should remain pure and the orchestration layer handles agent lifecycle/handoffs.
Common pitfalls and what to check
• Don’t copy chat_ctx before the tool output is recorded — that’s the root cause you described.
• If your environment automatically appends tool output to session.history, make sure your handoff code runs after that append completes.
• If you must trigger the handoff from inside the function tool, explicitly append the tool return to self.chat_ctx before copying it:
# inside function tool
tool_result = do_work(...)
# append tool return to chat context so it becomes visible
self.chat_ctx.add_message("tool", "function_name", tool_result)
# now handoff using the updated context
return TechnicalSupportAgent(chat_ctx=self.chat_ctx.copy(exclude_instructions=True))
(Use whichever API your chat_ctx provides for appending tool returns — the important part is the append happens before the copy.)
Questions to zero in on the exact change
• Where is your handoff triggered right now — inside the function tool return, or by an external orchestration layer after the function returns?
• Does your runtime automatically append the tool output to session.history, or do you need to append it manually inside the function?
• If you can paste the small handoff snippet (no secrets), I’ll show the exact location to insert the append and the chat_ctx.copy() call.
If you tell me which approach you’re using (in-function vs orchestration), I’ll give a minimal code change tailored to your setup.
, with interactive elementsboundless-computer-27062
03/26/2026, 9:24 AMtall-airline-16756
03/26/2026, 9:24 AMchat_ctx so it sees the function call and the tool output and won’t re-call the same function.
This example uses the patterns we discussed: a function-tool (here fetch_ticket_details) that returns a result, appends that result to the current agent's chat_ctx, and then returns a new agent instance constructed with chat_ctx=self.chat_ctx.copy(). Replace names and small API calls to match your actual LiveKit client classes and chat_ctx API if they differ.
# Pseudocode / minimal LiveKit-style example
class ChatContext:
# Minimal methods used in this example
def __init__(self):
self.history = [] # list of (role, name, content)
def add_message(self, role, name, content):
self.history.append((role, name, content))
def copy(self, exclude_instructions=False):
new = ChatContext()
# shallow copy of history preserves function calls and tool outputs
new.history = list(self.history)
return new
# Base agent class
class BaseAgent:
def __init__(self, chat_ctx: ChatContext):
self.chat_ctx = chat_ctx
def handle_user_message(self, message: str):
# Example entrypoint for the agent; in practice this triggers LLM etc.
self.chat_ctx.add_message("user", "user", message)
# Here the agent may decide to call a function tool
action = self.decide_action(message)
if action == "fetch_ticket":
# call the function tool and perform a handoff afterwards
return self.function_fetch_ticket_and_handoff(ticket_id="12345")
# otherwise continue normal handling
return "handled locally"
def decide_action(self, message):
# naive decision for demo purposes
if "ticket" in message:
return "fetch_ticket"
return "none"
# Example new agent to hand off to
class TechnicalSupportAgent(BaseAgent):
def __init__(self, chat_ctx: ChatContext):
super().__init__(chat_ctx)
def start(self):
# Example behavior: Inspect chat_ctx to decide what to do
for turn in self.chat_ctx.history:
print("HISTORY:", turn)
# Because we copied the updated chat_ctx, this agent will see the tool output
return "technical agent started"
# Example of a function-tool inside the agent
class CustomerServiceAgent(BaseAgent):
def __init__(self, chat_ctx: ChatContext):
super().__init__(chat_ctx)
def function_fetch_ticket_and_handoff(self, ticket_id: str):
# 1) LLM made a function call: record that in chat_ctx
# (In a real runtime this is recorded automatically as an assistant function call)
self.chat_ctx.add_message("assistant", "function_call:fetch_ticket", f'{{"ticket_id":"{ticket_id}"}}')
# 2) Perform the tool work (call external system)
tool_result = self._call_ticket_service(ticket_id)
# 3) Append the tool return value into the chat context BEFORE handoff
# (This is the critical step: ensure the runtime/chat_ctx includes the tool output)
self.chat_ctx.add_message("tool", "fetch_ticket", tool_result)
# 4) Now create the new agent using a copy of the updated chat_ctx
next_agent = TechnicalSupportAgent(chat_ctx=self.chat_ctx.copy(exclude_instructions=True))
# 5) Return or hand off the new agent instance
# In some systems you might return a special handoff signal containing the agent;
# here we'll just return the agent object for demonstration.
return next_agent
def _call_ticket_service(self, ticket_id: str):
# Simulate calling an external ticket system and returning details
return f"Ticket {ticket_id}: status=resolved, summary=User issue fixed"
# Demo of running the flow
if __name__ == "__main__":
# start conversation
initial_ctx = ChatContext()
agent = CustomerServiceAgent(chat_ctx=initial_ctx)
# user asks about a ticket -> agent decides to call function and then handoff
result = agent.handle_user_message("Please check ticket 12345")
# result is the new agent (TechnicalSupportAgent)
if isinstance(result, TechnicalSupportAgent):
print("Handoff successful. New agent sees history:")
output = result.start()
print(output)
else:
print("Flow result:", result)
Key points to adapt to your real LiveKit environment
• If your runtime automatically appends the LLM function call and the tool output into session.history, perform the handoff only after the runtime append completes. If you trigger the handoff inside the function tool, explicitly append the tool return to self.chat_ctx before copying the context.
• Use chat_ctx.copy() when constructing the new agent. Copy after the tool output is recorded so the new agent receives the full sequence: the LLM function call turn and the tool’s return turn.
• If your orchestration layer receives the function return and appends it to session.history, prefer doing the handoff there (construct new agent with chat_ctx=session.chat_ctx.copy() after append) to keep function tools pure.
If you want, I can:
• Convert this pseudocode into the exact LiveKit/SDK class names and methods you’re using if you share which LiveKit SDK (language and version) and the shape of your `chat_ctx`/session APIs.
• Show how to return a special handoff object or signal instead of returning an agent instance (if your runtime expects that)., with interactive elements