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 Lifecycle

Conversation Summarization & Decay

⏰ ~25 min to implement 📦 Requires: Dakera v0.11+

Transform long conversation histories into compact, persistent summaries while raw transcripts decay naturally. Ideal for legal, support, and long-running project workflows where outcomes matter more than raw dialogue.

Start Free →
Prerequisites
  • Running Dakera server (Quickstart)
  • An agent ID scoped per user or case
  • Optional: LLM client (OpenAI, Anthropic) for custom summarization prompts

The Problem: Unbounded Conversation Growth

Raw conversation logs are verbose. A single 90-minute legal consultation can generate 8,000+ tokens of dialogue. Storing every turn verbatim creates three compounding problems: recall noise (low-value messages compete with critical facts for retrieval slots), unbounded storage growth, and context window bloat when reconstructing agent state.

Human memory solves this naturally — you remember that a client wants to sue for wrongful termination, not the exact words they used in the second sentence of the third paragraph. Your AI agent needs the same ability: compress the transcript, persist the insight.

Pattern Context

This pattern works on two timescales simultaneously: real-time decay (raw messages fade as importance drops) and batch summarization (periodic consolidation compresses accumulated turns into a durable summary). Use both together for the best results.

Architecture Overview

The pattern combines two Dakera primitives: importance-weighted storage and built-in consolidation. Raw conversation turns are stored with low importance (0.2–0.4); decay lowers it further, so they stay below the recall threshold. Decay never deletes a memory: it lowers importance toward a floor of 0.01. LLM-generated summaries are stored at high importance (0.92 here) as semantic memories, and the agent's memory policy sets semantic_decay to flat so they keep that importance while the episodic turns fade.

Diagram: Summarization Pipeline

Workflow of the summarization pipeline across the conversation, your app and Dakera. Every turn is stored with store_memory() as a raw episodic memory at importance 0.2 to 0.4 that decays. Every 15 turns, at session end or on a topic shift, a trigger passes the recent turns to an LLM that extracts facts, decisions, dates and action items. The summary is stored as a semantic memory at importance 0.92, tagged summary and the case ID, and kept from decaying by a flat semantic decay policy. Consolidation, by AutoPilot every 12 hours or at once with consolidate_agent(), merges the old turns. If the LLM fails 3 times, the raw turns are joined and stored at 0.8 as a fallback. The next session recalls with min_importance 0.8 and gets summaries and key facts.Workflow of the summarization pipeline across the conversation, your app and Dakera. Every turn is stored with store_memory() as a raw episodic memory at importance 0.2 to 0.4 that decays. Every 15 turns, at session end or on a topic shift, a trigger passes the recent turns to an LLM that extracts facts, decisions, dates and action items. The summary is stored as a semantic memory at importance 0.92, tagged summary and the case ID, and kept from decaying by a flat semantic decay policy. Consolidation, by AutoPilot every 12 hours or at once with consolidate_agent(), merges the old turns. If the LLM fails 3 times, the raw turns are joined and stored at 0.8 as a fallback. The next session recalls with min_importance 0.8 and gets summaries and key facts.
Turns are stored cheaply, summaries are stored to last, and recall reads only the summaries.Full size ↗Full size ↗

Diagram: How a Raw Turn Decays

State diagram of a conversation turn. A raw turn is stored as an episodic memory at importance 0.2 to 0.4 with no TTL. As the session moves on it fades: decay lowers its importance toward a floor of 0.01 without deleting it, until it falls below min_importance 0.8 and drops out of recall. Before that, a summary taken every 15 turns or at session end keeps its content in a semantic summary at 0.92 that a flat semantic decay policy keeps in place, and consolidation can merge the fading turn once it is below 0.3.State diagram of a conversation turn. A raw turn is stored as an episodic memory at importance 0.2 to 0.4 with no TTL. As the session moves on it fades: decay lowers its importance toward a floor of 0.01 without deleting it, until it falls below min_importance 0.8 and drops out of recall. Before that, a summary taken every 15 turns or at session end keeps its content in a semantic summary at 0.92 that a flat semantic decay policy keeps in place, and consolidation can merge the fading turn once it is below 0.3.
A raw turn fades out of recall; what it said lives on in the summary.Full size ↗Full size ↗

Scenario: Lawbridge AI builds a legal assistant that attends client consultations. Each consultation generates 50–200 conversation turns. Attorneys need the case summary to persist across months; small talk and pleasantries should vanish within days.

Lawbridge stores each consultation turn at importance 0.25–0.35. After each consultation, they run a GPT-4 summarization prompt that extracts: client claims, relevant dates, requested remedies, and attorney notes. That summary is stored at importance 0.92 with tags ["summary", "case-{id}", "client-{id}"]. Semantic decay is set to flat for the agent, so when an attorney opens a case six months later, recall(min_importance=0.8) still returns the summaries, and no raw dialogue noise.

Result: attorneys get the complete case context back from one recall, without raw dialogue in the way.

Step-by-Step Implementation

  1. Store each conversation turn with low importance
    As the conversation progresses, write each user/assistant turn to Dakera with memory_type: "episodic" and importance 0.2–0.4. These are raw and verbose. Decay keeps lowering their importance (toward a floor of 0.01; it never deletes them), so they stay out of recall at min_importance=0.8. Leave ttl_seconds unset: AutoPilot never consolidates a memory that has a TTL.
  2. Detect summarization trigger
    Trigger summarization every 15 turns, at session end, or when a topic shift is detected. For legal workflows, always summarize at the explicit close of each consultation, not mid-conversation.
  3. Generate summary with an LLM
    Retrieve recent turns using search_memories or by recalling with low min_importance. Pass them to your LLM with a structured extraction prompt: "Extract key facts, decisions, dates, and action items." Validate the output before storing.
  4. Store the summary at high importance
    Store the LLM summary with importance: 0.92, memory_type: "semantic", and structured tags. Decay applies to every memory, so set semantic_decay to flat in the agent namespace's memory policy (_dakera_agent_{agent_id}); summaries then keep their importance and surface first in recall, while episodic turns keep decaying.
  5. Consolidate the old turns
    AutoPilot consolidates every 12 hours: clusters of three or more memories that share at least two tags (here turn and the case ID) and are all below importance 0.3 become one semantic memory. To run consolidation right after storing a summary, call consolidate_agent() (POST /v1/agents/{id}/consolidate).
  6. Recall with min_importance filter
    At the start of any future session, recall with min_importance=0.8 and relevant tags. This returns only durable summaries and key facts — no ephemeral noise from old conversations.

Before & After: Memory State

Before — raw turns (6 memories)
[
  {
    "id": "m-001",
    "content": "Client: Hi, thanks for seeing me",
    "importance": 0.2,
    "memory_type": "episodic",
    "created_at": "2026-03-10T09:00:00Z"
  },
  {
    "id": "m-002",
    "content": "Attorney: Of course, what brings you in?",
    "importance": 0.15,
    "memory_type": "episodic"
  },
  {
    "id": "m-003",
    "content": "Client: I was fired last Tuesday...",
    "importance": 0.35,
    "memory_type": "episodic"
  },
  {
    "id": "m-004",
    "content": "...after 8 years at the company.",
    "importance": 0.35,
    "memory_type": "episodic"
  },
  {
    "id": "m-005",
    "content": "Client: No severance was offered.",
    "importance": 0.4,
    "memory_type": "episodic"
  },
  {
    "id": "m-006",
    "content": "Attorney: Was there a written contract?",
    "importance": 0.25,
    "memory_type": "episodic"
  }
]
// Recall noise: 6 low-importance entries
// compete with critical facts
After — summarized (1 memory)
[
  {
    "id": "m-sum-001",
    "content": "CASE SUMMARY — March 10 2026: Client
Jane Doe, employed at Acme Corp for 8
years, terminated March 4 2026 without
cause or severance. No written contract.
Client seeks wrongful termination claim.
Attorney requested: employment records,
last 3 pay stubs, termination letter.
Next step: review docs by March 20.",
    "importance": 0.92,
    "memory_type": "semantic",
    "tags": [
      "summary",
      "case-247",
      "client-jane-doe",
      "wrongful-termination"
    ],
    "created_at": "2026-03-10T10:15:00Z"
  }
]
// Clean recall: 1 high-signal memory
// surfaces immediately at min_imp=0.8

Implementation

# 0. Once per agent: summaries (semantic) keep their importance, raw turns (episodic) still decay.
#    PUT replaces the whole policy; omitted fields take their defaults.
curl -X PUT http://localhost:3000/v1/namespaces/_dakera_agent_legal-assistant/memory_policy \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{"semantic_decay": "flat"}'

# 1. Store a conversation turn (low importance, decays)
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "legal-assistant",
    "content": "Client was terminated March 4 after 8 years. No severance.",
    "importance": 0.3,
    "memory_type": "episodic",
    "tags": ["turn", "case-247"]
  }'

# 2. Store the LLM-generated summary (high importance)
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "legal-assistant",
    "content": "CASE SUMMARY [case-247]: Jane Doe v Acme Corp: wrongful termination, 8yr tenure, no severance, no written contract. Docs requested: employment records, pay stubs, termination letter. Review by March 20.",
    "importance": 0.92,
    "memory_type": "semantic",
    "tags": ["summary", "case-247"]
  }'

# 3. Consolidate the agent's memories now (AutoPilot also does it every 12 hours)
curl -X POST http://localhost:3000/v1/agents/legal-assistant/consolidate \
  -H "Authorization: Bearer dk-..."

# 4. Next session: recall only summaries and key facts
curl -X POST http://localhost:3000/v1/memory/recall \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "legal-assistant", "query": "Jane Doe case details", "min_importance": 0.8, "top_k": 5}'
from dakera import DakeraClient
import openai

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

AGENT = "legal-assistant"
CASE_ID = "case-247"
SUMMARY_TRIGGER = 15  # summarize every 15 turns
MIN_RECALL_IMPORTANCE = 0.8

# Once per agent: summaries (semantic) keep their importance; raw turns (episodic) still decay
policy = client.get_memory_policy(f"_dakera_agent_{AGENT}")
policy.semantic_decay = "flat"
client.set_memory_policy(f"_dakera_agent_{AGENT}", policy)

conversation_turns = []

def store_turn(role: str, content: str):
    """Store a single conversation turn at low importance."""
    turn_text = f"{role.upper()}: {content}"
    conversation_turns.append(turn_text)
    client.store_memory(
        agent_id=AGENT,
        content=turn_text,
        importance=0.3,
        memory_type="episodic",
        tags=["turn", CASE_ID]
    )
    if len(conversation_turns) % SUMMARY_TRIGGER == 0:
        summarize_and_store()

def summarize_and_store():
    """Generate an LLM summary and store it at high importance."""
    turns_text = "\n".join(conversation_turns[-SUMMARY_TRIGGER:])
    response = oai.chat.completions.create(
        model="gpt-4o",
        messages=[{
            "role": "system",
            "content": "You are a legal case summarizer. Extract: client name, key claims, relevant dates, requested documents, and next steps. Be concise and factual."
        }, {
            "role": "user",
            "content": f"Summarize this consultation segment:\n\n{turns_text}"
        }]
    )
    summary = response.choices[0].message.content

    client.store_memory(
        agent_id=AGENT,
        content=f"CASE SUMMARY [{CASE_ID}]: {summary}",
        importance=0.92,
        memory_type="semantic",
        tags=["summary", CASE_ID, "auto-generated"]
    )
    # Consolidate now instead of waiting for AutoPilot's 12-hour cycle
    client.consolidate_agent(AGENT)

def recall_case_context(query: str):
    """Recall summaries and key facts, filtering out raw turns."""
    return client.recall(
        agent_id=AGENT,
        query=query,
        min_importance=MIN_RECALL_IMPORTANCE,
        top_k=5
    )

# --- Simulation ---
store_turn("client", "I was terminated on March 4 after 8 years. No severance was offered.")
store_turn("attorney", "Was there a written employment contract?")
store_turn("client", "No written contract. I was at-will.")
# ... more turns ...

# At session end: force a final summary regardless of the trigger
summarize_and_store()

# Next session: only durable summaries come back
context = recall_case_context("Jane Doe wrongful termination case")
for mem in context.memories:
    print(f"[{mem.importance:.2f}] {mem.content[:120]}")
import { DakeraClient } from '@dakera-ai/dakera';
import OpenAI from 'openai';

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

const AGENT = 'legal-assistant';
const CASE_ID = 'case-247';
const SUMMARY_TRIGGER = 15;

// Once per agent: summaries (semantic) keep their importance; raw turns still decay
const ns = `_dakera_agent_${AGENT}`;
const policy = await client.getMemoryPolicy(ns);
await client.setMemoryPolicy(ns, { ...policy, semantic_decay: 'flat' });

const conversationTurns: string[] = [];

async function storeTurn(role: 'client' | 'attorney', content: string) {
  const turnText = `${role.toUpperCase()}: ${content}`;
  conversationTurns.push(turnText);
  await client.storeMemory(AGENT, {
    content: turnText,
    importance: 0.3,
    memory_type: 'episodic',
    tags: ['turn', CASE_ID],
  });
  if (conversationTurns.length % SUMMARY_TRIGGER === 0) {
    await summarizeAndStore();
  }
}

async function summarizeAndStore() {
  const recentTurns = conversationTurns.slice(-SUMMARY_TRIGGER).join('\n');
  const response = await oai.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      {
        role: 'system',
        content: 'You are a legal case summarizer. Extract: client name, key claims, relevant dates, documents requested, and next steps. Be concise.'
      },
      { role: 'user', content: `Summarize this consultation:\n\n${recentTurns}` }
    ]
  });
  const summary = response.choices[0].message.content ?? '';

  await client.storeMemory(AGENT, {
    content: `CASE SUMMARY [${CASE_ID}]: ${summary}`,
    importance: 0.92,
    memory_type: 'semantic',
    tags: ['summary', CASE_ID, 'auto-generated'],
  });

  // Consolidate now instead of waiting for AutoPilot's 12-hour cycle
  await client.consolidateAgent(AGENT);
}

async function recallCaseContext(query: string) {
  return client.recall(AGENT, query, { min_importance: 0.8, top_k: 5 });
}

// Usage
await storeTurn('client', 'I was terminated on March 4 after 8 years. No severance.');
await storeTurn('attorney', 'Was there a written contract?');
await storeTurn('client', 'No written contract. I was at-will.');

// Force a final summary at session end
await summarizeAndStore();

// Retrieve clean context next session
const context = await recallCaseContext('Jane Doe wrongful termination');
context.memories.forEach(m => console.log(`[${m.importance}] ${m.content.slice(0, 120)}`));
use dakera_client::{DakeraClient, MemoryType, RecallRequest, StoreMemoryRequest};

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

// Once per agent: summaries (semantic) keep their importance; raw turns still decay
let ns = format!("_dakera_agent_{}", agent);
let mut policy = client.get_memory_policy(&ns).await?;
policy.semantic_decay = Some("flat".into());
client.set_memory_policy(&ns, policy).await?;

// Store raw conversation turns at low importance
client.store_memory(
    StoreMemoryRequest::new(agent, "CLIENT: Terminated March 4 after 8 years. No severance offered.")
        .with_type(MemoryType::Episodic)
        .with_importance(0.3)
        .with_tags(vec!["turn".into(), "case-247".into()]),
).await?;

// Store the LLM-generated summary at high importance
let summary = generate_case_summary(&turns).await?; // your LLM call
client.store_memory(
    StoreMemoryRequest::new(agent, format!("CASE SUMMARY [case-247]: {}", summary))
        .with_type(MemoryType::Semantic)
        .with_importance(0.92)
        .with_tags(vec!["summary".into(), "case-247".into()]),
).await?;

// Consolidate now instead of waiting for AutoPilot's 12-hour cycle
client.consolidate_agent(agent).await?;

// Recall only high-importance summaries next session
let context = client.recall(
    RecallRequest::new(agent, "Jane Doe wrongful termination case details")
        .with_min_importance(0.8)
        .with_top_k(5),
).await?;

for mem in &context.memories {
    let preview: String = mem.content.chars().take(120).collect();
    println!("[{:.2}] {}", mem.importance, preview);
}
package main

import (
    "context"
    "fmt"
    "log"

    dakera "github.com/dakera-ai/dakera-go"
)

func main() {
    client := dakera.NewClientWithOptions(dakera.ClientOptions{
        BaseURL: "http://localhost:3000",
        APIKey:  "dk-...",
    })
    ctx := context.Background()
    agent := "legal-assistant"
    caseID := "case-247"

    // Once per agent: summaries (semantic) keep their importance; raw turns still decay
    ns := "_dakera_agent_" + agent
    policy, err := client.GetMemoryPolicy(ctx, ns)
    if err != nil {
        log.Fatal(err)
    }
    flat := "flat"
    policy.SemanticDecay = &flat
    if _, err := client.SetMemoryPolicy(ctx, ns, *policy); err != nil {
        log.Fatal(err)
    }

    // Store a raw turn at low importance
    turnImp := float32(0.3)
    client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{
        Content:    "CLIENT: Terminated March 4 after 8 years. No severance.",
        Importance: &turnImp,
        MemoryType: "episodic",
        Tags:       []string{"turn", caseID},
    })

    // After summarization, store the summary at high importance
    summary := generateCaseSummary(turns) // your LLM call
    summaryImp := float32(0.92)
    client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{
        Content:    fmt.Sprintf("CASE SUMMARY [%s]: %s", caseID, summary),
        Importance: &summaryImp,
        MemoryType: "semantic",
        Tags:       []string{"summary", caseID},
    })

    // Consolidate now instead of waiting for AutoPilot's 12-hour cycle
    client.ConsolidateAgent(ctx, agent)

    // Recall only summaries at the next session
    minImp := float32(0.8)
    result, err := client.Recall(ctx, agent, dakera.RecallRequest{
        Query:         "Jane Doe wrongful termination case",
        MinImportance: &minImp,
        TopK:          5,
    })
    if err != nil {
        log.Fatal(err)
    }
    for _, mem := range result.Memories {
        fmt.Printf("[%.2f] %s\n", mem.Importance, mem.Content[:min(120, len(mem.Content))])
    }
}

Keep the insight, let the transcript fade

Decay and consolidation run server-side, with no infrastructure to manage.

Deploy Free →

SDK Reference

MethodSDKPurpose
store_memory(agent_id, content, importance, memory_type, tags)PythonStore turn or summary with importance score
storeMemory(agentId, {content, importance, memory_type, tags})TypeScriptStore turn or summary with importance score
client.store_memory(StoreMemoryRequest::new(agent_id, content)).await?RustStore turn or summary with importance score
client.StoreMemory(ctx, agentID, dakera.StoreMemoryRequest{...})GoStore turn or summary with importance score
recall(agent_id, query, min_importance, top_k)PythonRetrieve summaries above importance threshold
recall(agentId, query, {min_importance, top_k})TypeScriptRetrieve summaries above importance threshold
search_memories(agent_id, query)PythonFind recent turns before summarizing (POST /v1/memory/search)
searchMemories(agentId, query, {top_k})TypeScriptFind recent turns before summarizing
consolidate_agent(agent_id) / POST /v1/agents/{id}/consolidatePython / RESTConsolidate the agent's memories now
set_memory_policy(namespace, policy)PythonSet semantic_decay to flat so summaries keep their importance

Performance Considerations

15
Turns per summary in this pattern's code
0.8
min_importance that keeps raw turns out of recall
12 h
AutoPilot consolidation cycle

Edge Cases

Edge Case 1: Summarization Failure Mid-Session

If the LLM summarization call fails (timeout, rate limit), the raw turns remain at low importance. Always implement a retry with exponential backoff and a fallback: store the raw turns concatenated as a summary at importance 0.8 if the LLM fails after 3 attempts, so it still passes the recall filter. Never skip summary storage: the raw turns stay below the recall threshold.

Edge Case 2: Summarizing Before Enough Context Exists

Triggering summarization on fewer than 5 turns produces shallow summaries like "User greeted attorney." Guard against this with a minimum turn count of 8–10 before the first summarization trigger fires. Check len(conversation_turns) >= min_turns before calling the LLM.

Edge Case 3: Duplicate Summaries from Retries

If a session crashes after storing the summary but before clearing the turn buffer, a retry will generate and store a second summary covering the same turns. Deduplicate by tagging summaries with a deterministic ID: sha256(case_id + turn_range), then check for existing summaries with that tag before storing.

Edge Case 4: Recalling Raw Turns for Audit

Legal and compliance workflows sometimes need the original verbatim transcript, not just summaries. Store raw turns under a separate agent_id (e.g., legal-assistant-audit, which gets its own namespace) at importance 1.0, and give that namespace a memory policy with episodic_decay set to flat. The main agent's memories can decay freely while audit history keeps its importance.

Edge Case 5: Multi-Topic Sessions

If a single consultation covers multiple unrelated topics (e.g., client discusses both a contract dispute and estate planning), generate separate summaries per topic rather than one combined summary. Use topic detection (keyword-based or LLM-based) to split the turn buffer before summarizing. Tag each summary with its topic for cleaner recall.

Advanced Configuration: Decay Strategies & Summarization Tuning

Importance Levels by Memory Type

Memory TypeRecommended ImportanceReturned at min_importance=0.8
Raw greeting / small talk0.1–0.2No
Conversation turn (factual)0.3–0.4No
Working note / partial summary0.5–0.6No
Key fact / decision point0.8While decay keeps it at 0.8 or above
LLM-generated summary0.92Yes, with semantic_decay set to flat
Identity / case constants1.0Yes while above 0.8; store as semantic to keep it flat

Summarization Prompt Template

SYSTEM: You are a precise case-note summarizer for legal consultations.
Extract only facts stated by the client. Include:
1. Client full name (if mentioned)
2. Key legal claim(s) and relevant dates
3. Evidence or documents discussed
4. Attorney's requested next steps
5. Any deadlines or court dates mentioned

Output as a single paragraph under 200 words. Use past tense.
Do not infer, speculate, or add information not present in the transcript.

USER: Summarize the following consultation segment:
{TURNS}

Consolidation Schedule

For high-volume systems, consolidate on a schedule rather than after every summary. AutoPilot already runs every 12 hours; for a fixed time, such as a daily job at 2 AM local time, call consolidate_agent() from your own scheduler.

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.