Octopus
Episode Structure
An Episode is the complete record of one conversation session — from the first message to the session close. It captures messages, tool calls, agents involved, metadata, and an optional summary embedding for semantic search.
Episode Entity
public class Episode
{
public Guid Id { get; init; }
public Guid SessionId { get; init; } // = ConversationComposite.Id
public Guid AgentId { get; init; }
public Guid UserId { get; init; }
public string TenantId { get; init; }
// Content
public List<EpisodeMessage> Messages { get; set; }
public List<ToolCallRecord> ToolCalls { get; set; }
// Metadata
public DateTimeOffset StartedAt { get; init; }
public DateTimeOffset EndedAt { get; set; }
public EpisodeEndReason EndReason { get; set; } // UserClosed, Timeout, AgentClosed
// Summary for efficient search (generated at episode close)
public string? Summary { get; set; } // LLM-generated summary of the session
public float[]? SummaryEmbedding { get; set; } // embedding of the summary
// Audit
public int MessageCount { get; set; }
public int TotalInputTokens { get; set; }
public int TotalOutputTokens { get; set; }
}
EpisodeMessage
public class EpisodeMessage
{
public int Ordinal { get; set; } // position in conversation
public string Role { get; set; } // "user", "assistant", "system", "tool"
public string Content { get; set; }
public DateTimeOffset Timestamp { get; set; }
public Guid? AgentId { get; set; } // for multi-agent episodes
public bool ContainsPII { get; set; } // flagged by PII detector
public string? RedactedContent { get; set; } // PII-scrubbed version
}
Episode Summary Generation
When a session ends, Octopus optionally generates a summary of the conversation using a lightweight LLM call. This summary is embedded and stored — enabling efficient semantic search across episodes without loading all messages:
// Episode closure and summary generation
public async Task CloseEpisodeAsync(Episode episode, bool generateSummary = true)
{
episode.EndedAt = DateTimeOffset.UtcNow;
if (generateSummary && episode.MessageCount > 2)
{
var summaryPrompt = BuildSummaryPrompt(episode.Messages);
var summaryResponse = await _summaryLLM.CompleteAsync(
new[] { new LLMMessage(Role.User, summaryPrompt) },
tools: null,
new LLMOptions { MaxOutputTokens = 200 });
episode.Summary = summaryResponse.Content;
episode.SummaryEmbedding = await _embeddingProvider
.EmbedAsync(episode.Summary);
}
await _store.StoreAsync(episode);
}
Episode in Database
The episode is stored in the Octopus_Episodes SQL table. The message list is stored as a JSON column:
| Column | Type | Notes |
|---|---|---|
| Id | uniqueidentifier | Primary key |
| SessionId | uniqueidentifier | FK to Octopus_Conversations |
| AgentId | uniqueidentifier | FK to Octopus_Agents |
| UserId | uniqueidentifier | FK to Octopus_Users |
| TenantId | nvarchar(100) | Tenant isolation key |
| MessagesJson | nvarchar(max) | JSON array of EpisodeMessage |
| Summary | nvarchar(2000) | LLM-generated summary |
| SummaryEmbedding | varbinary(max) | float[] serialized as binary |
| StartedAt / EndedAt | datetimeoffset | Session time range |
| MessageCount / TotalTokens | int | Usage statistics |