The smooth handoff
The transfer works, but the new agent does not know who the caller is or why they called. That is the handoff feeling most voice systems get wrong. The fix is to copy the previous agent chat context into the new one, truncate it to the last few messages, and inject a fresh system prompt.
async def on_enter(self) -> None:
agent_name = self.__class__.__name__
userdata: UserData = self.session.userdata
if userdata.ctx and userdata.ctx.room:
await userdata.ctx.room.local_participant.set_attributes(
{"agent": agent_name}
)
chat_ctx = self.chat_ctx.copy()
# Copy recent history from whichever agent was active before
if userdata.prev_agent:
items_copy = self._truncate_chat_ctx(
userdata.prev_agent.chat_ctx.items,
keep_function_call=True,
)
existing_ids = {item.id for item in chat_ctx.items}
items_copy = [i for i in items_copy if i.id not in existing_ids]
chat_ctx.items.extend(items_copy)
# Fresh system prompt describing this agent role
chat_ctx.add_message(
role="system",
content=f"You are the {agent_name}. {userdata.summarize()}",
)
await self.update_chat_ctx(chat_ctx)
self.session.generate_reply()The hook copies the last N turns from the previous agent, deduplicates by item id so shared history does not repeat, and injects a system message for the new role. The new agent walks in with the story but knows its own job.
def _truncate_chat_ctx(
self,
items: list,
keep_last_n_messages: int = 6,
keep_system_message: bool = False,
keep_function_call: bool = False,
) -> list:
"""Keep a tight window of recent turns."""
def _valid(item) -> bool:
if not keep_system_message and item.type == "message" and item.role == "system":
return False
if not keep_function_call and item.type in ("function_call", "function_call_output"):
return False
return True
new_items = []
for item in reversed(items):
if _valid(item):
new_items.append(item)
if len(new_items) >= keep_last_n_messages:
break
new_items.reverse()
while new_items and new_items[0].type in ("function_call", "function_call_output"):
new_items.pop(0)
return new_itemsTruncation keeps the context useful without dragging in old system prompts from the previous role. Keeping the last six turns is a sensible default. Tune up for long calls, down for short transactional ones.
Ordering exercise: Order the steps of a smooth handoff
Loading practice…
Two reasons. First, the old system prompt describes a different role and will confuse the new agent. Second, long histories cost tokens and latency on every turn. Keeping a short window of recent exchanges gives the new agent enough context to continue without pulling the previous role along for the ride.
Quiz: Quiz
Loading practice…
Checkpoint: Multi-agent architecture checkpoint
Loading practice…