NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee what's new →
Intermediate Architecture

Session Handoff

⏱ ~20 min to implement 📦 Requires: Dakera v0.12+

Transfer full conversation context between agents (bot to human, specialist to specialist, or across devices) without making the user start over. The session handoff pattern uses Dakera's sessions and memories to capture and persist exactly what the receiving agent needs, so it continues from where the previous one stopped.

Implement Session Handoff →
Prerequisites
  • Running Dakera server (see Quickstart)
  • An API key with write access to the agent's namespace (sessions are authorized by their agent's namespace)
  • A multi-agent or bot-to-human escalation setup

Problem

Conversations frequently cross agent boundaries. A tier-1 support bot hits its knowledge limit and escalates to a human agent. A general assistant identifies a specialist problem and routes to a domain expert. A user switches from mobile to desktop mid-conversation. In each case, without a proper handoff mechanism, the receiving agent starts cold:

The session handoff pattern stores structured context at handoff time and makes it the first thing the receiving agent reads, so they start with full awareness of the situation, the user's emotional state, and what has already been attempted.

What Makes a Good Handoff Summary

A handoff summary should contain three things: (1) the user's goal — what they are trying to accomplish, (2) the situation — what has been tried, what worked, what failed, and the current state, (3) the emotional context — frustration level, urgency, any commitments made. Facts alone are not enough — the receiving agent needs to know how to approach the user, not just what the problem is.

Architecture

The pattern has two distinct phases — the outgoing session (source agent) and the incoming session (target agent). Each phase uses different Dakera operations.

Handoff Flow Diagram

Sequence diagram of a bot to human handoff. In session 1 the source agent calls start_session(), stores events with store_memory and its session_id (problem, attempts and sentiment at importance 0.7 to 0.85), then, once the handoff trigger fires, stores the summary at importance 1.0 with the handoff target and reason in its metadata, and calls end_session with a short summary. In session 2 the target agent calls start_session with handoff_from, then recall with the handoff tag and the source session_id, and gets the handoff summary back first.Sequence diagram of a bot to human handoff. In session 1 the source agent calls start_session(), stores events with store_memory and its session_id (problem, attempts and sentiment at importance 0.7 to 0.85), then, once the handoff trigger fires, stores the summary at importance 1.0 with the handoff target and reason in its metadata, and calls end_session with a short summary. In session 2 the target agent calls start_session with handoff_from, then recall with the handoff tag and the source session_id, and gets the handoff summary back first.
The summary is stored before the session ends, so the receiving agent's first recall, filtered by its tag, starts with it.Full size ↗Full size ↗

Step-by-Step Implementation

Implementation

# ===== SOURCE AGENT (Bot) =====

# 1. Start the bot session
curl -X POST http://localhost:3000/v1/sessions/start \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "support-bot", "metadata": {"channel": "webchat", "user_id": "user-u_9k3m"}}'
# Response: {"id": "sess_bot_7f2a", ...}

# 2. Store context during the conversation
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "support-bot",
    "content": "User reports charge of $89.99 on Jan 15 that they did not authorize. Transaction ID: TXN-88291.",
    "importance": 0.8,
    "session_id": "sess_bot_7f2a",
    "tags": ["billing", "dispute", "unauth-charge"]
  }'

curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "support-bot",
    "content": "Attempted: verified account ownership (success), ran standard refund flow (failed: transaction too old), offered account credit (rejected by user).",
    "importance": 0.75,
    "session_id": "sess_bot_7f2a",
    "tags": ["attempted-resolution", "failed-paths"]
  }'

# 3. Store the handoff summary at maximum importance, handoff details in its metadata
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "support-bot",
    "content": "HANDOFF SUMMARY: User Maria Santos (user-u_9k3m) disputes $89.99 charge TXN-88291 from Jan 15. Standard refund flow failed (30-day limit). User rejected credit offer and wants a full refund only. Sentiment: frustrated (3rd contact about this). Recommended: supervisor override for refund. Bot promised escalation within 5 minutes.",
    "importance": 1.0,
    "session_id": "sess_bot_7f2a",
    "tags": ["handoff", "session-summary", "priority:high"],
    "metadata": {"handoff_to": "human-agent", "escalation_reason": "refund-flow-failed"}
  }'

# 4. End the bot session (its memories stay queryable)
curl -X POST http://localhost:3000/v1/sessions/sess_bot_7f2a/end \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"summary": "Escalated to a human agent: refund flow failed, supervisor override needed."}'

# ===== TARGET AGENT (Human) =====

# 5. Start the human agent's session, linked to the original
curl -X POST http://localhost:3000/v1/sessions/start \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "support-bot", "metadata": {"handler": "human-sarah", "handoff_from": "sess_bot_7f2a", "channel": "webchat"}}'

# 6. Fetch the handoff summary first (tag filter), then the rest of the source session
curl -X POST http://localhost:3000/v1/memory/recall \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "support-bot", "query": "billing dispute handoff", "tags": ["handoff"], "session_id": "sess_bot_7f2a", "top_k": 1}'

curl -X POST http://localhost:3000/v1/memory/recall \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "support-bot", "query": "billing dispute refund attempts", "session_id": "sess_bot_7f2a", "top_k": 8}'

# Fallback when no summary was stored: every memory of the source session
curl http://localhost:3000/v1/sessions/sess_bot_7f2a/memories \
  -H "Authorization: Bearer $DAKERA_API_KEY"
from dakera import DakeraClient

client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")


class HandoffSession:
    """A source session that ends with a structured handoff summary."""

    def __init__(self, agent_id: str, user_id: str, channel: str = "webchat"):
        self.agent_id = agent_id
        self.user_id = user_id
        self.channel = channel
        self.session_id = None
        self.attempted_resolutions = []

    def start(self) -> str:
        session = client.start_session(
            agent_id=self.agent_id,
            metadata={"user_id": self.user_id, "channel": self.channel},
        )
        self.session_id = session["id"]
        return self.session_id

    def store_event(self, content: str, event_type: str, importance: float = 0.75):
        """Store a conversation event bound to this session."""
        return client.store_memory(
            agent_id=self.agent_id,
            content=content,
            importance=importance,
            session_id=self.session_id,
            tags=["event", event_type],
        )

    def record_attempt(self, action: str, outcome: str):
        record = f"Tried: {action}. Outcome: {outcome}."
        self.attempted_resolutions.append(record)
        return self.store_event(record, "attempted-resolution", importance=0.7)

    def handoff(self, target_agent: str, reason: str, sentiment: str,
                user_goal: str, recommended_action: str, urgency: str = "normal") -> dict:
        """1. Store the summary at importance 1.0  2. End the session."""
        attempts = " ".join(self.attempted_resolutions) or "No prior attempts."
        summary = client.store_memory(
            agent_id=self.agent_id,
            content=(f"HANDOFF SUMMARY [{urgency.upper()}]: User goal: {user_goal}. "
                     f"Situation: {attempts} User sentiment: {sentiment}. "
                     f"Recommended next action: {recommended_action}."),
            importance=1.0,
            session_id=self.session_id,
            tags=["handoff", "session-summary", f"priority:{urgency}"],
            metadata={"handoff_to": target_agent, "escalation_reason": reason, "urgency": urgency},
        )
        client.end_session(self.session_id, summary=f"Escalated to {target_agent}: {reason}")
        return {"source_session_id": self.session_id, "handoff_memory_id": summary["id"],
                "target_agent": target_agent, "urgency": urgency}


class IncomingSession:
    """Target agent session: loads the handoff context before saying anything."""

    def __init__(self, agent_id: str, handler_id: str, handoff_data: dict):
        self.agent_id = agent_id
        self.handler_id = handler_id
        self.source = handoff_data["source_session_id"]

    def start_and_load_context(self, query: str, top_k: int = 8):
        client.start_session(
            agent_id=self.agent_id,
            metadata={"handler": self.handler_id, "handoff_from": self.source, "channel": "webchat"},
        )
        # The summary first, by tag; then the rest of the source session
        summary = client.recall(agent_id=self.agent_id, query=query,
                                tags=["handoff"], session_id=self.source, top_k=1).memories
        rest = client.recall(agent_id=self.agent_id, query=query,
                             session_id=self.source, top_k=top_k).memories
        seen = {m.id for m in summary}
        return summary + [m for m in rest if m.id not in seen]

    @staticmethod
    def opening_message(context) -> str:
        if context and context[0].content.startswith("HANDOFF SUMMARY"):
            return ("I've just been briefed on your situation and I have the full context "
                    "from your previous conversation. Let me take it from here.")
        return "How can I help you today?"


# ===== USAGE: Customer Support Escalation =====
bot = HandoffSession(agent_id="support-bot", user_id="user-u_9k3m")
bot.start()
bot.store_event("User: Maria Santos reports unauthorized charge $89.99 on Jan 15. Transaction: TXN-88291.",
                event_type="issue-report", importance=0.85)
bot.record_attempt("Standard refund flow for TXN-88291", "FAILED: transaction exceeds 30-day refund window")
bot.record_attempt("Offered $90 account credit as alternative", "REJECTED: user insists on full refund only")
bot.store_event("User expressed frustration: 'This is the third time I am calling about this.'",
                event_type="sentiment", importance=0.85)

handoff_data = bot.handoff(
    target_agent="human-agent",
    reason="refund-window-exceeded-supervisor-required",
    sentiment="High frustration: 3rd contact, previous resolution attempts failed",
    user_goal="Full refund of $89.99 unauthorized charge TXN-88291",
    recommended_action="Supervisor override refund. Mention 5-min ETA already committed.",
    urgency="high",
)

human = IncomingSession(agent_id="support-bot", handler_id="agent-sarah", handoff_data=handoff_data)
context = human.start_and_load_context("billing dispute refund situation and what was tried")
for mem in context:
    print(f"  [{mem.importance:.2f}] {mem.content[:100]}")
print(IncomingSession.opening_message(context))
import { DakeraClient } from '@dakera-ai/dakera';

const client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-...' });
const AGENT = 'support-bot';

// ===== SOURCE AGENT (Bot) =====

// 1. Start the bot session
const botSession = await client.startSession(AGENT, { user_id: 'user-u_9k3m', channel: 'webchat' });

// 2. Store context events during the conversation
await client.storeMemory(AGENT, {
  content: 'User Maria Santos disputes $89.99 charge TXN-88291 from Jan 15. Not authorized.',
  importance: 0.85,
  session_id: botSession.id,
  tags: ['billing', 'dispute', 'unauth-charge'],
});
await client.storeMemory(AGENT, {
  content: 'Standard refund flow failed: transaction exceeds 30-day window. Credit offer ($90) rejected.',
  importance: 0.75,
  session_id: botSession.id,
  tags: ['attempted-resolution', 'failed'],
});

// 3. Store the handoff summary at maximum importance
const { memory: summary } = await client.storeMemory(AGENT, {
  content: [
    'HANDOFF SUMMARY [HIGH]:',
    'User: Maria Santos (user-u_9k3m).',
    'Goal: Full refund of $89.99 unauthorized charge TXN-88291 (Jan 15).',
    'Tried: standard refund (30-day limit exceeded), credit offer (rejected).',
    'Sentiment: high frustration, 3rd contact.',
    'Committed: escalation within 5 minutes.',
    'Action: supervisor override refund required.',
  ].join(' '),
  importance: 1.0,
  session_id: botSession.id,
  tags: ['handoff', 'session-summary', 'priority:high'],
  metadata: { handoff_to: 'human-sarah', escalation_reason: 'refund-flow-failed' },
});

// 4. End the bot session (endSession sends a summary from dakera-js 0.12.2)
await client.endSession(botSession.id, { summary: 'Escalated to a human agent: refund flow failed.' });

// ===== TARGET AGENT (Human) =====

// 5. Start the human agent's session, linked to the original
await client.startSession(AGENT, { handler: 'human-sarah', handoff_from: botSession.id });

// 6. The summary first (tag filter), then the rest of the source session
const query = 'billing dispute refund situation attempted resolutions';
const first = await client.recall(AGENT, query, { tags: ['handoff'], session_id: botSession.id, top_k: 1 });
const rest = await client.recall(AGENT, query, { session_id: botSession.id, top_k: 8 });
const context = [...first.memories, ...rest.memories.filter(m => m.id !== summary.id)];

context.forEach(mem => console.log(`  [${mem.importance.toFixed(2)}] ${mem.content.substring(0, 100)}`));

// 7. The human agent acknowledges continuity
const opening = context[0]?.content.startsWith('HANDOFF SUMMARY')
  ? "I have the full context from your previous conversation. I'm going to resolve this for you now."
  : 'Let me pull up your case.';
console.log(`Agent opening: "${opening}"`);
use dakera_client::{DakeraClient, RecallRequest, StoreMemoryRequest};
use serde_json::json;

let client = DakeraClient::builder("http://localhost:3000").api_key("dk-...").build()?;
let agent = "support-bot";

// ===== SOURCE AGENT =====
let session = client
    .start_session_with_metadata(agent, json!({"user_id": "user-u_9k3m", "channel": "webchat"}))
    .await?;

client.store_memory(
    StoreMemoryRequest::new(agent, "User reports $89.99 unauthorized charge TXN-88291 on Jan 15.")
        .with_importance(0.85)
        .with_tags(vec!["billing".into(), "dispute".into()])
        .with_session(session.id.clone()),
).await?;

// Handoff summary at importance 1.0, then end the session with a short summary
client.store_memory(
    StoreMemoryRequest::new(agent, concat!(
        "HANDOFF SUMMARY [HIGH]: User Maria Santos disputes $89.99 TXN-88291 Jan 15. ",
        "Standard refund failed (30d limit). Credit rejected. Third contact. ",
        "Sentiment: high frustration. Action: supervisor override refund required.",
    ))
    .with_importance(1.0)
    .with_tags(vec!["handoff".into(), "session-summary".into(), "priority:high".into()])
    .with_session(session.id.clone()),
).await?;
client.end_session(&session.id, Some("Escalated to a human agent".to_string())).await?;

// ===== TARGET AGENT =====
client
    .start_session_with_metadata(agent, json!({"handler": "human-sarah", "handoff_from": session.id}))
    .await?;

// Importance 1.0 ranks the summary high; to filter by tag and session, use the
// REST body (tags, session_id) as in the REST tab. Here: pick it out by its prefix.
let found = client.recall(
    RecallRequest::new(agent, "billing dispute refund situation and resolution attempts").with_top_k(8),
).await?;
if let Some(summary) = found.memories.iter().find(|m| m.content.starts_with("HANDOFF SUMMARY")) {
    println!("Handoff summary: {}", summary.content);
}
ctx := context.Background()
client := dakera.NewClientWithOptions(dakera.ClientOptions{
    BaseURL: "http://localhost:3000",
    APIKey:  os.Getenv("DAKERA_API_KEY"),
})
agent := "support-bot"

// ===== SOURCE AGENT =====
session, _ := client.StartSession(ctx, dakera.StartSessionRequest{
    AgentID:  agent,
    Metadata: map[string]interface{}{"user_id": "user-u_9k3m", "channel": "webchat"},
})

eventImp, summaryImp := float32(0.85), float32(1.0)
client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{
    Content:    "User Maria Santos disputes $89.99 charge TXN-88291 from Jan 15. Not authorized.",
    Importance: &eventImp,
    Tags:       []string{"billing", "dispute"},
    SessionID:  session.ID,
})

// Handoff summary at importance 1.0, then end the session
client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{
    Content: "HANDOFF SUMMARY [HIGH]: User: Maria Santos. Goal: refund $89.99 TXN-88291 Jan 15. " +
        "Tried: standard refund (30d limit failed), credit ($90 rejected). 3rd contact. High frustration. " +
        "Action: supervisor override required. Bot committed 5-min ETA.",
    Importance: &summaryImp,
    Tags:       []string{"handoff", "session-summary", "priority:high"},
    SessionID:  session.ID,
})
client.EndSession(ctx, session.ID) // EndSession sends no summary in dakera-go

// ===== TARGET AGENT =====
client.StartSession(ctx, dakera.StartSessionRequest{
    AgentID:  agent,
    Metadata: map[string]interface{}{"handler": "human-sarah", "handoff_from": session.ID},
})

// Importance 1.0 ranks the summary high; to filter by tag and session, use the
// REST body (tags, session_id) as in the REST tab. Here: pick it out by its prefix.
found, _ := client.Recall(ctx, agent, dakera.RecallRequest{
    Query: "billing dispute refund situation attempts",
    TopK:  8,
})
for _, m := range found.Memories {
    if strings.HasPrefix(m.Content, "HANDOFF SUMMARY") {
        fmt.Printf("Handoff summary: %s\n", m.Content)
        break
    }
}

Stop making users repeat themselves at every escalation.

Dakera's sessions and memories give receiving agents the full context from their first recall call.

Implement Session Handoff →

Before / After Memory State

Before — no handoff pattern
-- Session 1 ends --
[memories exist but no structured summary]
[no session linkage to Session 2]

-- Human agent starts Session 2 --
context = recall("billing issue")

[0.5] User called about billing
[0.5] Charge on Jan 15
[0.5] Tried to refund
[0.5] User was frustrated

Agent: "Can you describe your issue?"
User: "I ALREADY EXPLAINED THIS THREE TIMES"

Result: CSAT score: 2/5
Resolution time: +18 minutes
After — session handoff pattern
-- Handoff summary stored at importance 1.0 --

-- Human agent recalls with tags=["handoff"], then the session --
[1.0][handoff] HANDOFF SUMMARY: Maria Santos,
  $89.99 TXN-88291 Jan 15, standard refund
  failed (30d limit), credit rejected, 3rd
  contact, HIGH frustration. Action: supervisor
  override. Bot promised 5-min ETA.

[0.85] Unauthorized charge confirmed
[0.75] Refund flow: 30-day limit exceeded
[0.90] User frustration escalating

Agent: "I have the full context — I'm applying
  a supervisor override for your refund now."
User: "Finally, thank you."

Result: CSAT score: 5/5. Time saved: 12 min

SDK Reference

OperationPythonTypeScriptPurpose
Start sessionstart_session(agent_id, metadata)startSession(agentId, metadata)Begin a tracked conversation session
Store eventstore_memory(..., session_id=id)storeMemory(agentId, {..., session_id: id})Bind memories to the current session
Store handoff summarystore_memory(..., importance=1.0, tags=["handoff"], metadata={...})storeMemory(agentId, {..., importance: 1.0, tags: ['handoff']})Highest-importance context for the receiver
End sessionend_session(session_id, summary=...)endSession(sessionId, {summary})Close the source session
List sessionslist_sessions(agent_id=...)listSessions({agent_id})Audit session history
Session memoriesGET /v1/sessions/{id}/memoriesEvery memory of a session (the fallback)
Recall the summaryrecall(agent_id, query, tags=["handoff"], session_id=src, top_k=1)recall(agentId, query, {tags: ['handoff'], session_id: src, top_k: 1})Load the handoff summary in the new session

Real-World Example: 24/7 Support with Shift Handoffs

A SaaS company runs customer support 24/7 with 8-hour agent shifts. At shift change, dozens of open cases need to transfer to incoming agents. Here is how session handoff works at scale:

08:00 — Day shift starts
Night shift agent Sarah runs the handoff sequence for 12 open cases. Each case gets a structured handoff summary stored at importance 1.0 in a loop over the open cases. The summary's metadata includes handoff_to: "day-shift" and the estimated resolution complexity.
08:01 — Day agent loads queue
Day agent Marcus runs recall() with tags=["handoff"] for each open case using the customer's agent_id, and gets each case's handoff summary back. Marcus can triage all 12 cases in under 3 minutes by reading only the summaries, with no need to re-read conversation history.
Throughout the day — context accumulates
As Marcus resolves each case, he stores resolution notes with importance 0.7. For complex cases requiring further escalation, he runs the handoff pattern again — creating a chain of handoff summaries that form a complete audit trail of every step taken.
Analytics — handoff quality metrics
The ops team reads each handoff session's memories (GET /v1/sessions/{id}/memories) weekly to analyze handoff efficiency: resolution time before vs. after handoffs, cases that required re-explanation (indicating poor handoff quality). These metrics drive continuous improvement of the summary generation process.

Handoff Package: Anatomy of a Context Transfer

This diagram breaks down exactly what gets captured in a handoff package and how the receiving agent reconstructs the full context from stored memories alone.

Data flow of a handoff package. The source agent writes session events (problem, attempts, sentiment, commitments), a handoff summary (goal, situation, what was tried, mood, next action) with handoff_to and reason in its metadata, and end_session() with a short summary. Dakera keeps them as event memories at importance 0.7 to 0.85, a summary memory at importance 1.0 tagged handoff and session-summary, and an ended session that stays queryable. The target agent reads the summary with a recall filtered by the handoff tag, then the events with a recall scoped to the source session, and falls back to GET /v1/sessions/{id}/memories if no summary was stored.Data flow of a handoff package. The source agent writes session events (problem, attempts, sentiment, commitments), a handoff summary (goal, situation, what was tried, mood, next action) with handoff_to and reason in its metadata, and end_session() with a short summary. Dakera keeps them as event memories at importance 0.7 to 0.85, a summary memory at importance 1.0 tagged handoff and session-summary, and an ended session that stays queryable. The target agent reads the summary with a recall filtered by the handoff tag, then the events with a recall scoped to the source session, and falls back to GET /v1/sessions/{id}/memories if no summary was stored.
Everything the receiving agent needs is in stored memories; a tag filter fetches the summary and the session listing covers a missing one.Full size ↗Full size ↗

Performance Considerations

2
Recall calls to load a handoff: the summary, then the session
4 h
Default idle timeout before the server ends a session (v0.12.2)
> 3
Memories in a session for Dakera to store its own summary when it ends

Loading handoff context is two recall() calls, not a session replay or history re-fetch: one filtered by the handoff tag and one scoped to the source session with session_id. The tag filter matters because recall ranks by relevance, importance and recency together (0.55, 0.20 and 0.13 on the default route), so importance 1.0 raises the summary's rank without guaranteeing first place.

Edge Cases

1. Handoff summary too long for effective recall

A bot stuffs 2,000 words into the handoff summary, filling the receiving agent's context window with handoff boilerplate and leaving no room for the agent's response. Fix: enforce a strict length limit on handoff summaries — 200–300 words maximum. Use a bulleted structure: Goal, Situation, Tried, Sentiment, Action. The full conversation history is available via additional recall queries if needed.

2. Chain handoffs losing earlier context

Case escalates: Bot → Tier 1 Agent → Tier 2 Specialist → Manager. By the fourth handoff, only the Tier 2 → Manager handoff summary is at importance 1.0. Earlier context gets buried. Fix: each handoff summary should include a handoff_chain field summarizing all prior handoffs. The current summary absorbs the previous one: "This is the 3rd escalation. Previous agents confirmed X, tried Y, Z."

3. Target agent starts responding before recall completes

In async/streaming architectures, the human agent's UI loads before the recall API call returns. The agent responds without context. Fix: block the agent UI from showing the user as "connected" until the context recall completes. Show a "Loading context..." indicator; it eliminates cold-start responses entirely.

4. Source session ends before handoff summary is stored

The bot crashes or times out before completing the handoff sequence. No handoff summary exists. Fix: implement a two-phase handoff: (1) store the handoff summary memory first, (2) then end the session. If the session ends without a handoff memory, the target agent falls back to GET /v1/sessions/{id}/memories for the source session ID; if that session held more than three memories, the summary memory Dakera stored when it ended is among them. A session the bot never ends is ended by the server after its idle timeout.

5. Multiple simultaneous escalations of the same user

A user contacts support on two channels simultaneously (chat and phone). Two bot sessions both escalate to human agents. Both human agents pull the same user's memories and see each other's in-progress context. Fix: include a channel tag on all memories and the handoff summary. Agents filter recall by channel to avoid seeing the other channel's state. Merge conflicts are resolved by the supervisor if both cases need to be collapsed.

Include Emotional State in Every Handoff

Factual context alone is not enough. "User disputes $89.99 charge" tells the human agent what to do but not how to approach the user. "User is highly frustrated after 3 contacts — open with an apology and direct commitment to resolve before asking any verification questions" changes the entire interaction quality. Include explicit sentiment guidance and tone recommendations in every handoff summary.

Advanced Configuration: Batch Handoffs & Session Analytics

Batch Handoff at Shift Change

from dakera import DakeraClient

client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")

def batch_handoff_shift_change(open_cases: list[dict]) -> list[dict]:
    """Store a handoff summary for every open case at shift change."""
    packages = []
    for case in open_cases:
        context = client.recall(
            agent_id=case["agent_id"],
            query=f"{case['issue_type']} situation and resolution status",
            top_k=5,
        )
        summary_text = generate_handoff_summary(case, context.memories)  # your LLM call
        mem = client.store_memory(
            agent_id=case["agent_id"],
            content=summary_text,
            importance=1.0,
            tags=["handoff", "shift-change", f"case:{case['id']}"],
            metadata={"handoff_to": "day-shift"},
        )
        packages.append({"case_id": case["id"], "agent_id": case["agent_id"],
                         "handoff_memory_id": mem["id"], "priority": case.get("priority", "normal")})
    return sorted(packages, key=lambda x: x["priority"] == "high", reverse=True)

Session Analytics: Measuring Handoff Quality

import requests

def session_memories(session_id: str) -> list[dict]:
    """GET /v1/sessions/{id}/memories: every memory of a session."""
    r = requests.get(f"http://localhost:3000/v1/sessions/{session_id}/memories",
                     headers={"Authorization": "Bearer dk-..."})
    r.raise_for_status()
    data = r.json()
    return data["memories"] if isinstance(data, dict) else data

def analyze_handoff_quality(agent_id: str) -> dict:
    """Share of handoff sessions whose source session holds a handoff summary."""
    sessions = client.list_sessions(agent_id=agent_id)
    handoffs = [s for s in sessions if (s.get("metadata") or {}).get("handoff_from")]
    with_summary = sum(
        any("HANDOFF SUMMARY" in m["content"] for m in session_memories(s["metadata"]["handoff_from"]))
        for s in handoffs
    )
    return {"total_handoffs": len(handoffs),
            "summary_rate": with_summary / max(len(handoffs), 1)}

Cross-Channel Handoff (Mobile to Desktop)

# Mobile session context stored normally
client.store_memory(
    agent_id="assistant",
    content="User was reviewing Q3 financial report on mobile, viewing page 12 (EBITDA breakdown).",
    importance=0.8,
    tags=["cross-channel", "session-state", "mobile"],
)

# Mobile handoff summary
client.store_memory(
    agent_id="assistant",
    content="CHANNEL HANDOFF: User switching mobile->desktop. Was on Q3 report page 12 (EBITDA). Had questions about depreciation line items. Resume from that context.",
    importance=1.0,
    tags=["handoff", "cross-channel", "mobile-to-desktop"],
)

# Desktop session fetches the channel handoff summary by its tag
context = client.recall(
    agent_id="assistant",
    query="current session state and what user was working on",
    tags=["mobile-to-desktop"],
    top_k=1,
)
Try it live

Run this pattern against a real Dakera.

The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.