Session Handoff
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 →- 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 human agent asks "Can you describe your problem?" — the user has already explained it twice to the bot
- The specialist agent has no context on what was already tried
- The desktop session has no idea what was discussed on mobile 10 minutes ago
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.
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
Step-by-Step Implementation
-
Store key context throughout the source sessionDuring the bot conversation, store memories for important events as they happen: the user's stated problem, steps already tried, any commitments made ("I'll escalate this for you"), and sentiment signals ("User is frustrated — third time calling about this"). Use importance 0.7–0.85 for these — high enough to surface during handoff recall, but below the handoff summary itself.
-
Detect the handoff triggerTriggers are application-specific: user requests human help, bot confidence falls below threshold, issue classification requires escalation, wait time exceeds SLA, or the user expresses high frustration. When the trigger fires, begin the handoff sequence immediately — do not continue attempting to resolve the issue after the decision to escalate.
-
Generate and store the handoff summary at importance 1.0The handoff summary is the single most important piece of context the receiving agent will see. Store it with
importance: 1.0, the sourcesession_idand the handoff target and reason in itsmetadata. Importance raises its rank, but recall also weighs relevance and recency, so the receiving agent fetches it directly with a tag filter. Include: (1) user's goal, (2) current situation, (3) what was tried and what happened, (4) any commitments made by the bot, (5) urgency and emotional state, (6) recommended next action. Tag it with["handoff", "session-summary"]. -
End the source sessionCall
end_session()with a short summary. The session's memories remain queryable after it ends; they are not deleted. When a session with more than three memories ends, Dakera also stores a semantic summary memory of it. A session the bot never ends is ended by the server after its idle timeout (4 hours by default, v0.12.2). -
Start the target agent's session linked to the originalStart a new session for the receiving agent with
handoff_frommetadata pointing to the source session ID. This allows analytics tools to reconstruct the full conversation chain across sessions and measure handoff quality (did context transfer result in faster resolution?). -
Recall context as the first action in the new sessionBefore the receiving agent says anything, call
recall()with a query about the user's issue, thehandofftag and the sourcesession_idto get the summary, then a second recall scoped to the source session for the rest. The agent can then open with: "I see you've been dealing with a billing discrepancy from January 15th. I have the full context from the bot and I'm going to resolve this for you right now." This acknowledgment of continuity dramatically improves customer satisfaction scores.
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.
Before / After Memory State
-- 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
-- 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
| Operation | Python | TypeScript | Purpose |
|---|---|---|---|
| Start session | start_session(agent_id, metadata) | startSession(agentId, metadata) | Begin a tracked conversation session |
| Store event | store_memory(..., session_id=id) | storeMemory(agentId, {..., session_id: id}) | Bind memories to the current session |
| Store handoff summary | store_memory(..., importance=1.0, tags=["handoff"], metadata={...}) | storeMemory(agentId, {..., importance: 1.0, tags: ['handoff']}) | Highest-importance context for the receiver |
| End session | end_session(session_id, summary=...) | endSession(sessionId, {summary}) | Close the source session |
| List sessions | list_sessions(agent_id=...) | listSessions({agent_id}) | Audit session history |
| Session memories | GET /v1/sessions/{id}/memories | Every memory of a session (the fallback) | |
| Recall the summary | recall(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:
handoff_to: "day-shift" and the estimated resolution complexity.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.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.
Performance Considerations
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.
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,
)
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.