Portal Community

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:

ColumnTypeNotes
IduniqueidentifierPrimary key
SessionIduniqueidentifierFK to Octopus_Conversations
AgentIduniqueidentifierFK to Octopus_Agents
UserIduniqueidentifierFK to Octopus_Users
TenantIdnvarchar(100)Tenant isolation key
MessagesJsonnvarchar(max)JSON array of EpisodeMessage
Summarynvarchar(2000)LLM-generated summary
SummaryEmbeddingvarbinary(max)float[] serialized as binary
StartedAt / EndedAtdatetimeoffsetSession time range
MessageCount / TotalTokensintUsage statistics