Portal Community

The Graph-Structured Model

Octopus uses a composite object pattern — each composite aggregates a cluster of related domain objects into a single, cohesive entity. This avoids the N+1 query problem and ensures that all agent logic receives a fully-hydrated object, not a shallow database row.

The three composites form a graph:

AgentComposite

The AgentComposite is the complete definition of an AI agent. It is created by a platform administrator or AI engineer in the agents-app micro-frontend and persisted to the Octopus_Agents SQL table.

public class AgentComposite
{
    public Guid Id { get; init; }
    public string Name { get; set; }
    public string TenantId { get; init; }

    // LLM Configuration
    public LLMConfig LLMConfig { get; set; }          // provider, model, temperature
    public ILLMProvider LLMProvider { get; set; }      // resolved at runtime

    // Memory
    public MemoryConfig MemoryConfig { get; set; }     // which types enabled, TTLs
    public MemoryOrchestrator MemoryOrchestrator { get; set; } // wired at startup

    // Tools and Plugins
    public ToolRegistry ToolRegistry { get; set; }     // all MCP tools available
    public IReadOnlyList<IOctopusPlugin> Plugins { get; set; }

    // Behavior
    public AgentPersona Persona { get; set; }          // name, tone, language
    public string SystemPrompt { get; set; }           // assembled by SystemPromptBuilder
    public AgentGoal Goal { get; set; }                // structured goal definition
    public BehavioralConstraints Constraints { get; set; } // what the agent must not do
}
Hydration at Startup

AgentComposites are loaded and hydrated at application startup (or on first use) and cached in IAgentRepository. LLM providers and plugin instances are resolved once from DI — not on every request.

ConversationComposite

The ConversationComposite represents one active conversation session — from the first user message to the session end. It is the unit of episodic memory capture.

public class ConversationComposite
{
    public Guid Id { get; init; }            // = sessionId
    public Guid AgentId { get; init; }
    public Guid UserId { get; init; }
    public string TenantId { get; init; }

    // Message History (working memory)
    public List<LLMMessage> Messages { get; set; }

    // Tool Call History
    public List<ToolCallRecord> ToolCalls { get; set; }

    // Session Metadata
    public DateTimeOffset StartedAt { get; init; }
    public DateTimeOffset? EndedAt { get; set; }
    public ConversationStatus Status { get; set; }  // Active, Closed, Expired

    // Injected Context (assembled per turn by WorkingMemoryManager)
    public WorkingMemoryContext CurrentContext { get; set; }
}

UserComposite

The UserComposite represents the human (or system) interacting with agents. It tracks which agents a user can access and links to their conversation history.

public class UserComposite
{
    public Guid Id { get; init; }
    public string ExternalUserId { get; set; }    // from auth provider
    public string TenantId { get; init; }
    public string DisplayName { get; set; }
    public UserPreferences Preferences { get; set; }  // language, timezone

    // Agent Access
    public IReadOnlyList<Guid> AccessibleAgentIds { get; set; }

    // Conversation History (references only — not loaded unless needed)
    public IReadOnlyList<Guid> ConversationIds { get; set; }
}

Composite Relationships

RelationshipCardinalityNotes
User → AgentsMany-to-manyAccess controlled via UserComposite.AccessibleAgentIds
User → ConversationsOne-to-manyEach session creates a new ConversationComposite
Agent → ConversationsOne-to-manyMany users can talk to the same agent simultaneously
Conversation → AgentMany-to-oneA conversation is always with exactly one agent
Conversation → UserMany-to-oneA conversation is always with exactly one user

Composite Loading Pattern

Composites are loaded via repository interfaces — never via direct DbContext access from application code:

// Loading an agent for a conversation
var agent = await _agentRepository.GetCompositeAsync(agentId, tenantId);

// Loading a conversation (creates if new session)
var conversation = await _conversationRepository
    .GetOrCreateAsync(sessionId, agentId, userId, tenantId);

// Loading a user's accessible agents
var user = await _userRepository.GetCompositeAsync(userId, tenantId);
var accessibleAgents = user.AccessibleAgentIds;
Tenant Isolation is Enforced at Composite Level

Every repository method requires a tenantId parameter. The underlying queries always add a WHERE TenantId = @tenantId filter. Cross-tenant composite access is not possible through the standard repository layer.