Octopus
Recalling Past Episodes
Episodic recall is the process by which an agent retrieves relevant past conversation episodes before responding. Octopus supports three recall modes: recency-based (most recent first), semantic search (similarity to current message), and session-specific lookup.
Recall Modes
| Mode | Method | When to Use |
|---|---|---|
| Recency | RecallRecentAsync(agentId, userId, limit) | Default — always inject the N most recent sessions |
| Semantic | SearchAsync(agentId, userId, query, topK) | Find episodes topically relevant to the current message |
| Session | GetBySessionAsync(sessionId) | Load a specific past session for replay or inspection |
Semantic Recall Implementation
public async Task<IReadOnlyList<EpisodeSnippet>> SearchAsync(
Guid agentId, Guid userId, string query, int topK = 3,
CancellationToken ct = default)
{
// Embed the query
var queryEmbedding = await _embeddingProvider.EmbedAsync(query, ct);
// Load episode summary embeddings for this user+agent
var episodes = await _db.Episodes
.Where(e => e.AgentId == agentId
&& e.UserId == userId
&& e.SummaryEmbedding != null
&& !e.IsArchived)
.Select(e => new { e.Id, e.Summary, e.SummaryEmbedding, e.StartedAt })
.ToListAsync(ct);
// Rank by cosine similarity
var ranked = episodes
.Select(e => new
{
e.Id,
e.Summary,
e.StartedAt,
Score = CosineSimilarity(queryEmbedding, DeserializeEmbedding(e.SummaryEmbedding!))
})
.OrderByDescending(e => e.Score)
.Take(topK)
.Where(e => e.Score >= _config.MinRecallScore) // threshold filter
.ToList();
return ranked.Select(e => new EpisodeSnippet
{
EpisodeId = e.Id,
Summary = e.Summary!,
Date = e.StartedAt,
RelevanceScore = e.Score
}).ToList();
}
Injecting Recalled Episodes into Context
The MemoryOrchestrator formats recalled episodes as a context block for the LLM:
private string FormatEpisodeContext(IReadOnlyList<EpisodeSnippet> episodes)
{
if (!episodes.Any()) return string.Empty;
var sb = new StringBuilder();
sb.AppendLine("[Past Conversation Context]");
sb.AppendLine("Relevant past conversations with this user:");
foreach (var ep in episodes.OrderBy(e => e.Date))
{
sb.AppendLine($"- {ep.Date:yyyy-MM-dd}: {ep.Summary}");
}
return sb.ToString();
// Injected as a system-role message before the user's message
}
Recall Configuration
// In MemoryConfig on AgentComposite
public class EpisodicRecallConfig
{
public bool Enabled { get; set; } = true;
public int RecentSessionsToInject { get; set; } = 2; // always inject last N sessions
public int SemanticTopK { get; set; } = 3; // semantic search results
public float MinRecallScore { get; set; } = 0.65f; // below this, don't inject
public bool UseSessionSummaries { get; set; } = true; // summary vs. full messages
}
Summary vs. Full Messages
By default, recalled episodes are injected as summaries (much shorter), not full message threads. This preserves token budget. If an agent needs to recall exact wording from past sessions, set UseSessionSummaries = false — but be aware this significantly increases token usage.