ChanlChanl
Testing & Evaluation

Testing AI agent handoffs before they break in production

Multi-agent CX systems fail at the seam between agents. Here's how to build handoff tests that catch context loss, duplicate actions, and loop failures before your customers feel them.

DGDean GroverCo-founderFollow
July 1, 2026
12 min read
Two agent nodes connected by an arrow, representing a customer conversation flowing between a routing agent and a specialist agent

A customer calls about a billing dispute. Your intake agent captures the account ID, the nature of the issue, and the fact that this is the customer's third call about the same problem. It decides the case needs a specialist. The handoff fires.

The billing specialist agent opens with: "Hi, how can I help you today?"

Three seconds of good intake work, erased by one bad handoff. The customer who just spelled out their account number twice for two different agents hears this and escalates to a human immediately. In logs, everything looks fine: HTTP 200, handoff completed, new session active. The failure only shows up in post-call surveys and churn data.

That's the handoff problem. And it's the most expensive seam in any multi-agent CX system, because it's invisible in single-agent testing and invisible in turn-level logs.

What actually breaks during a handoff

Context loss is the most common failure mode. The receiving agent starts with no context from the sender and treats the incoming session as a new conversation. From the customer's perspective, the agent just forgot everything. Every piece of information they provided needs to be provided again.

Context corruption is the second failure mode. The sending agent assembles a context package, but it's partial: the customer's intent is present, but the tool results aren't. The billing specialist knows the customer called about a dispute but doesn't know what the intake agent already fetched. It queries the same CRM record the intake agent already retrieved, adds 600ms of latency, and surfaces information it could have started with.

Duplicate actions are the third and most expensive. Both the intake agent and the specialist believe they're responsible for the same operation. A CRM update executes twice. A refund triggers twice. In production, duplicate actions are invisible until a reconciliation report or a customer calls back confused about two refunds.

Loop failures are the fourth. The specialist agent determines it can't resolve the issue and passes back to intake. Intake re-routes to the specialist. Your logs show active sessions. Your customer is stuck in a circle. Most systems don't have explicit loop detection, which means this particular failure can run for longer than you'd expect before anyone notices.

Failure modeVisibilityCustomer impactDetection
Context lossLow (HTTP 200)Customer repeats themselvesPost-call CSAT, manual review
Context corruptionLowDuplicate tool calls, added latencyLatency spike, duplicate log entries
Duplicate actionsVery lowWrong CRM state, double chargesReconciliation reports
Loop failureMediumCustomer stuck, no resolutionSession duration, escalation flag

Building the context package contract

Before you can test a handoff, you need to define what the handoff transfers. Most multi-agent systems fail here not because of a technical problem but because the context package is never explicitly defined. One agent assumes the other will figure it out. The other agent starts from scratch.

Define the context schema before you write any agent logic. The schema is the contract between your agents, and the handoff test is testing compliance with that contract.

handoff-schema.ts·typescript
interface AgentHandoffPackage {
  customerId: string;
  sessionId: string;
  handoffReason: 'specialist_required' | 'escalation' | 'routing';
  customerIntent: string;        // what the customer is trying to accomplish
  conversationSummary: string;   // brief summary of the session so far
  emotionalState: 'neutral' | 'frustrated' | 'angry' | 'satisfied';
  toolResults: Record<string, unknown>;  // all tool outputs from this session
  openToolCalls: string[];       // tool calls pending at handoff time
  constraints: string[];         // instructions for the receiving agent
  priorHandoffs: number;         // number of times this session has been transferred
}

The openToolCalls field matters more than it looks. If the intake agent was mid-fetch when it decided to transfer, the tool result will arrive after the handoff fires. The receiving agent needs to know it's coming, or it will fetch the same data again. The constraints array is how the sending agent passes instructions that shouldn't be in the customer's conversation: "do not put this customer on hold," "a $10 credit was already declined," "this is the customer's third call this week."

Testing in three layers

Once you have the schema, handoff testing breaks into three distinct layers: the sending agent's output, the receiving agent's initialization, and the end-to-end session.

Layer 1: Context package assembly. Test the sending agent in isolation. Does it produce a valid context package on a clean handoff? What does it produce when it hands off mid-tool-call? What happens if the handoff trigger fires before the customer has provided their account details?

context-assembly.test.ts·typescript
describe('intake agent context package', () => {
  it('produces valid package on clean handoff', async () => {
    const session = await simulateIntakeSession({
      customerInput: "I've been charged twice for my subscription and this is the third time I'm calling",
      triggerHandoff: true,
      capturePackage: true,
    });
 
    const pkg = session.handoffPackage;
    expect(pkg.customerIntent).toBe('duplicate_charge_dispute');
    expect(pkg.toolResults['crm_lookup']).toBeDefined();
    expect(pkg.openToolCalls).toHaveLength(0);
    expect(pkg.constraints).toContain('third contact about same issue');
    expect(pkg.priorHandoffs).toBe(0);
  });
 
  it('flags open tool calls at handoff time', async () => {
    const session = await simulateIntakeSession({
      customerInput: "checking my account",
      triggerHandoffDuringToolCall: 'crm_lookup',
      capturePackage: true,
    });
 
    const pkg = session.handoffPackage;
    expect(pkg.openToolCalls).toContain('crm_lookup');
  });
 
  it('includes prior handoff count', async () => {
    const session = await simulateIntakeSession({
      priorHandoffs: 2,
      capturePackage: true,
    });
    expect(session.handoffPackage.priorHandoffs).toBe(2);
  });
});

Layer 2: Receiving agent initialization. Give the receiving agent a context package and verify it starts from the right state. It should not re-ask for information already in the package. It should not re-execute tool calls whose results are in toolResults. It should acknowledge the constraints from the sender.

init-from-handoff.test.ts·typescript
describe('billing agent initialization from handoff', () => {
  it('does not ask for information already in the context package', async () => {
    const pkg: AgentHandoffPackage = {
      customerId: 'cust_12345',
      sessionId: 'sess_99',
      handoffReason: 'specialist_required',
      customerIntent: 'duplicate_charge_dispute',
      emotionalState: 'frustrated',
      conversationSummary: 'Customer charged twice on 2026-06-28. Account confirmed.',
      toolResults: {
        crm_lookup: { accountStatus: 'active', lastCharge: '2026-06-28', chargeCount: 2 }
      },
      openToolCalls: [],
      constraints: ['third contact this week -- do not offer standard hold'],
      priorHandoffs: 0,
    };
 
    const response = await billingAgent.initializeFromHandoff(pkg);
    const firstTurn = response.firstAgentMessage;
 
    // Should not re-ask for account ID or re-explain the situation
    expect(firstTurn).not.toMatch(/what('?s| is) (your account|the issue)/i);
    expect(firstTurn).not.toMatch(/can you (tell|give) me/i);
    // Should acknowledge the known situation
    expect(firstTurn).toMatch(/duplicate charge/i);
  });
 
  it('does not re-execute tool calls whose results are in the package', async () => {
    const pkg = makeHandoffPackage({
      toolResults: { crm_lookup: { accountStatus: 'active' } },
      openToolCalls: [],
    });
 
    const session = await billingAgent.initializeFromHandoff(pkg);
    const toolCalls = session.toolCallLog;
 
    // CRM lookup already in package -- should not be called again
    expect(toolCalls.filter((t) => t.name === 'crm_lookup')).toHaveLength(0);
  });
});

Layer 3: End-to-end session. Run both agents as a single session from the customer's perspective. This is where you catch repeat-question failures that only appear when both sides run together, and where you validate that the customer experience is continuous across the boundary.

Testing the edge cases

The happy-path handoff is straightforward. The edge cases are where multi-agent systems actually fail in production.

Mid-tool-call transfers. The intake agent was fetching the customer's order history when it decided to transfer. The tool result arrives 400ms after the handoff package was assembled. The billing specialist needs to know the result is coming and wait for it, not trigger a duplicate fetch. Test this explicitly by injecting a delay in the tool response and verifying both sides handle it correctly.

Angry customer transfers. The sending agent established that the customer is frustrated after being transferred twice already. The constraint "do not put this customer on hold" must travel through the context package and be honored by the receiving agent. Test that frustrated customers don't get placed on hold, transferred again without resolution, or asked to re-verify their identity.

Loop detection. The billing specialist determines it can't resolve the issue and routes back to intake. Intake re-routes to the billing specialist. Test that your system detects this pattern and routes to human escalation after one loop, not after the customer has been transferred six times.

High prior-handoff count. When priorHandoffs is 2 or more, the receiving agent should recognize this customer has already been passed around and treat resolution as a priority. Test that agents actually change their behavior based on this signal.

Concurrent sessions. Two customers call at the same time, both transferred to the billing agent with different context packages. Test that the packages stay isolated and that customer A's tool results don't appear in customer B's session.

Running scenario tests across the handoff boundary

With Chanl's scenario testing, you can define multi-agent flows as a single test that runs both agents as a connected session. The scenario describes the agents, the handoff trigger, the customer persona, and the expected outcome across the full conversation:

handoff-scenario.ts·typescript
import Chanl from '@chanl/sdk';
 
const chanl = new Chanl({ apiKey: process.env.CHANL_API_KEY });
 
const scenario = await chanl.scenarios.create({
  name: 'billing dispute: third contact',
  agents: ['intake-agent-v3', 'billing-specialist-v2'],
  flow: {
    entrypoint: 'intake-agent-v3',
    handoffTrigger: { intent: 'billing_dispute', threshold: 0.85 },
    handoffTarget: 'billing-specialist-v2',
    loopDetection: { maxHandoffs: 2, fallback: 'human_escalation' },
  },
  persona: {
    name: 'Emily Chen',
    issue: 'Charged twice on 2026-06-28. Third call about this issue.',
    emotionalState: 'frustrated',
    behavior: 'will not repeat her account number a second time',
  },
  successCriteria: [
    'customer did not repeat account number after handoff',
    'billing agent acknowledged prior contacts',
    'dispute initiated within 3 turns of handoff',
    'hold was not offered',
  ],
});
 
const result = await chanl.scenarios.run(scenario.id);
console.log(result.scorecard);
// { passed: 4, failed: 0, handoffPackageValid: true, contextCompleteness: 1.0 }

The scenario result includes the per-agent transcript, the actual context package that was transferred, schema validation results, and a scorecard against your success criteria.

Layer 1: Sending agent produces context package Schema validation Layer 2: Receiving agent initializes from package No re-asking, no duplicate tool calls Layer 3: End-to-end session test Customer experience continuous across handoff Scorecard: passed / failed
Multi-agent handoff test layers

What to monitor in production

Once your handoff tests pass and you've deployed, the monitoring layer needs to track handoffs as a distinct category of event.

Four metrics are worth instrumenting from day one.

Handoff success rate: the percentage of handoffs where the context package was delivered, accepted, and validated. A success rate below 99% means your schema contract has gaps or your sending agent is producing invalid packages intermittently.

Context completeness score: for each required field in your schema, what percentage of transferred packages included it? If customerIntent is present 100% of the time but emotionalState is present 60% of the time, the sending agent isn't consistently populating that field.

Post-handoff repeat-question rate: within two turns of a handoff, did the customer mention their name, account number, or issue description again? This is the metric that correlates most directly with CSAT. You can detect it automatically from transcripts without needing human review.

End-to-end resolution rate for multi-agent sessions: how does the resolution rate for sessions that crossed a handoff boundary compare to single-agent sessions? If multi-agent sessions resolve at 73% and single-agent sessions resolve at 87%, the handoff is where value is leaking.

Chanl's analytics surfaces per-handoff-pair metrics automatically. If the intake-to-billing handoff has a 12% post-handoff repeat-question rate and the intake-to-scheduling handoff has a 3% rate, you know which interface to fix without manually reading transcripts. The monitoring layer can alert when any metric crosses a threshold, so you catch handoff degradation before it becomes a CSAT problem.

The seam is the system

Single-agent testing is necessary. It's not sufficient for multi-agent systems. Two agents that each perform well in isolation can fail completely at the boundary between them. The handoff is not a technical detail. It's the moment the customer feels whether your system works as a coherent whole or as disconnected bots that don't talk to each other.

The pattern that works: define the context package schema before writing any agent, test each layer of the interface separately, run end-to-end scenario tests that span the boundary, and monitor the handoff as a first-class metric in production.

Start with the schema. It's the contract. Everything else flows from it.

Test your agent handoffs before they break

Run multi-agent scenario tests that span the handoff boundary. Catch context loss, duplicate actions, and loop failures in staging, not in a post-incident review.

Start Testing
DG

Co-founder

Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.

Learn Agentic AI

Weekly. Patterns for shipping agents that work. MCP, scorecards, regression tests, prompts, model comparisons.

500+ builders subscribed

Frequently Asked Questions