Agent Handoff
Agent handoff is the mechanism by which one Octopus agent transfers an active conversation to another agent. The handoff preserves context so the receiving agent can continue without requiring the user to repeat themselves.
Handoff Context
When a handoff occurs, a HandoffContext object is passed to the target agent. It contains exactly what the target agent needs to continue the conversation intelligently:
public class HandoffContext
{
public Guid SourceAgentId { get; set; }
public Guid ConversationId { get; set; }
public string UserMessage { get; set; } // the message that triggered routing
public string HandoffReason { get; set; } // why the handoff was initiated
public List<LLMMessage> RecentHistory { get; set; } // last N messages from the conversation
public Dictionary<string, object> ContextData { get; set; } // key facts extracted by orchestrator
public string SummaryForTarget { get; set; } // brief summary for the target agent's context
}
AgentHandoffService
public class AgentHandoffService : IAgentHandoffService
{
public async Task<HandoffResult> TransferAsync(
Guid conversationId,
Guid targetAgentId,
HandoffContext context,
CancellationToken ct = default)
{
// 1. Load target agent
var targetAgent = await _agentRepo.GetCompositeAsync(targetAgentId, context.TenantId);
// 2. Build the target agent's initial context with handoff summary
var handoffMessage = new LLMMessage(Role.System,
$"[Handoff from {context.SourceAgentId}] {context.SummaryForTarget}");
// 3. Update conversation to reference the new agent
await _conversationRepo.UpdateAgentAsync(conversationId, targetAgentId);
// 4. Invoke target agent with handoff context
var response = await _agentExecutor.ExecuteAsync(
targetAgent,
conversation: await _conversationRepo.GetAsync(conversationId),
newMessage: context.UserMessage,
prependMessages: new[] { handoffMessage },
ct: ct);
return new HandoffResult
{
TargetAgentId = targetAgentId,
Response = response,
HandoffSucceeded = true
};
}
}
Context Included in Handoff
| Item | Always Included | Configurable |
|---|---|---|
| User's triggering message | Yes | No |
| Handoff reason (from orchestrator) | Yes | No |
| Summary for target agent | Yes | Template in team config |
| Recent message history | No | MaxHandoffHistoryMessages setting |
| Tool call history | No | IncludeToolHistoryOnHandoff setting |
| Full conversation history | No | Not recommended — use episodic memory instead |
User Experience During Handoff
By default, handoffs are transparent to the user — they continue chatting without knowing which agent responded. Optionally, the chat-app can be configured to show a "Now speaking with: HR Agent" indicator:
// TypeScript: chat-app handoff event
interface HandoffEvent {
type: 'agent_handoff';
fromAgent: { id: string; displayName: string };
toAgent: { id: string; displayName: string };
handoffReason: string;
showToUser: boolean; // controlled by team config
}
// SSE event sent during handoff:
// event: handoff
// data: {"type":"agent_handoff","toAgent":{"id":"...","displayName":"HR Agent"},"showToUser":true}
A handoff keeps the same ConversationComposite — the session ID does not change. This means episodic memory still accumulates correctly. The conversation will appear as a single session in the user's history, even though multiple agents responded.