I am trying to implement an `AgentHandoff` within ...
# ask-ai
b
I am trying to implement an
AgentHandoff
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.
✅ 1
t
Thanks for reaching out, Dinesh. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — that description is clear and helpful. You're seeing the next agent re-call the same function because it doesn't have the tool's output in its conversation history. I’ll explain the fix and then ask one quick question to confirm your setup. Short answer / fix • When you create the new agent during the handoff, pass the previous agent’s chat context into the new agent so the full conversation (including the function call and the tool’s returned output) is visible. • Use
self.chat_ctx.copy()
when constructing the new agent. For example:
Copy code
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 elements
b
if i do
chat_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 completion
t
Thanks — that clarification helps. You’re right to be concerned: copying
chat_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:
Copy code
# 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 elements
b
give me a working sample livekit example above implementation.
t
Thanks — I’ll give you a minimal, self-contained LiveKit-style example that shows the two important pieces: • Ensure the function/tool result is appended to the conversation history before creating the next agent. • Create the next agent with a copy of the updated
chat_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.
Copy code
# 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