This document describes Pattern's memory hierarchy, native agent coordination via groups, and background processing strategies.
Pattern uses a tiered memory system optimized for ADHD cognitive support:
Small blocks (~2000 chars each) always present in the agent's context window:
- persona: Agent identity and capabilities
- current_state: Real-time status (energy, attention, mood)
- active_context: Current task and recent important events
- bond_evolution: Relationship dynamics and trust building
Active context that can be loaded or archived:
- Pinned blocks: Always loaded when agent processes messages
- Ephemeral blocks: Loaded only when referenced by notifications
DataStream sources create Working blocks for their content:
// Source creates block for new content
memory.create_block(
agent_id,
"discord_thread_12345",
"Discord thread context",
BlockType::Working,
BlockSchema::text(),
2000,
).await?;
// Block is included via batch_block_ids when processing notification
let request = ContextBuilder::new(&memory, &config)
.for_agent(agent_id)
.with_batch_blocks(vec!["discord_thread_12345".to_string()])
.build()
.await?;Separate from blocks, searchable via FTS5 + sqlite-vec:
- Agent observations and insights
- Partner behaviour patterns over time
- Accumulated wisdom
// Store insight
memory.insert_archival(
agent_id,
"User works best with time estimates multiplied by 1.5x",
Some(json!({"category": "time_patterns"})),
).await?;
// Search later
let results = memory.search_archival(agent_id, "time estimates", 10).await?;Conversation history with Snowflake ordering:
- Complete message history with batching
- Recursive summarization for context window management
- FTS5 search across all messages
Permissions control who can modify memory content:
| Permission | Read | Append | Overwrite | Delete |
|---|---|---|---|---|
| ReadOnly | Yes | No | No | No |
| Partner | Yes | Request | Request | No |
| Human | Yes | Request | Request | No |
| Append | Yes | Yes | No | No |
| ReadWrite | Yes | Yes | Yes | No |
| Admin | Yes | Yes | Yes | Yes |
PartnerandHumanpermissions require approval viaPermissionBroker- Requests appear as Discord/CLI notifications for humans to approve
Loro CRDT documents support typed schemas:
Text: Simple text with optional viewport
BlockSchema::Text { viewport: None }Map: Structured key-value fields
BlockSchema::Map {
fields: vec![
FieldDef { name: "energy".into(), field_type: FieldType::Number, ..Default::default() },
FieldDef { name: "mood".into(), field_type: FieldType::Text, ..Default::default() },
],
}List: Ordered collections (tasks, items)
BlockSchema::List { item_schema: None, max_items: Some(20) }Log: Append-only with display limit
BlockSchema::Log {
display_limit: 10,
entry_schema: LogEntrySchema { timestamp: true, agent_id: true, fields: vec![] },
}Composite: Multiple sections
BlockSchema::Composite {
sections: vec![
CompositeSection { name: "notes".into(), schema: Box::new(BlockSchema::text()), read_only: false },
CompositeSection { name: "diagnostics".into(), schema: Box::new(BlockSchema::text()), read_only: true },
],
}Agents can share blocks with each other:
// Share a block with another agent (ReadOnly permission)
db.share_block(owner_id, block_id, recipient_id, MemoryPermission::ReadOnly).await?;
// Recipient sees it in their shared blocks
let shared = memory.list_shared_blocks(recipient_id).await?;
// Access shared content
let doc = memory.get_shared_block(recipient_id, owner_id, "shared_notes").await?;Shared blocks appear in context with attribution:
<block:shared_notes permission="ReadOnly" shared_from="Archive">
Cross-agent insights about user patterns
</block:shared_notes>Pattern's native coordination replaces external dependencies like Letta.
A constellation is a collection of agents for a specific user:
pub struct Constellation {
pub id: ConstellationId,
pub owner_id: UserId,
pub name: String,
pub agents: Vec<(AgentModel, ConstellationMembership)>,
pub groups: Vec<GroupId>,
}Groups define how agents coordinate:
pub struct AgentGroup {
pub id: GroupId,
pub name: String,
pub description: String,
pub coordination_pattern: CoordinationPattern,
pub state: GroupState,
pub members: Vec<(AgentModel, GroupMembership)>,
}Six native coordination patterns:
One agent leads, delegates to others:
CoordinationPattern::Supervisor {
leader_id: pattern_id,
delegation_rules: DelegationRules {
max_delegations_per_agent: Some(3),
delegation_strategy: DelegationStrategy::Capability,
fallback_behavior: FallbackBehavior::HandleSelf,
},
}Agents take turns in order:
CoordinationPattern::RoundRobin {
current_index: 0,
skip_unavailable: true,
}Agents vote on decisions:
CoordinationPattern::Voting {
quorum: 3,
voting_rules: VotingRules {
voting_timeout: Duration::from_secs(30),
tie_breaker: TieBreaker::Random,
weight_by_expertise: true,
},
}Sequential processing through stages:
CoordinationPattern::Pipeline {
stages: vec![
PipelineStage {
name: "analysis".into(),
agent_ids: vec![entropy_id],
timeout: Duration::from_secs(60),
on_failure: StageFailureAction::Skip,
},
PipelineStage {
name: "scheduling".into(),
agent_ids: vec![flux_id],
timeout: Duration::from_secs(60),
on_failure: StageFailureAction::Retry { max_attempts: 2 },
},
],
parallel_stages: false,
}Context-based agent selection:
CoordinationPattern::Dynamic {
selector_name: "capability".into(),
selector_config: HashMap::from([
("preferred_domain".into(), "task_management".into()),
]),
}Available selectors:
random: Random selectioncapability: Match message content to agent capabilitiesload_balancing: Select least recently used agentsupervisor: LLM-based selection by supervisor agent
Background monitoring with intervention triggers:
CoordinationPattern::Sleeptime {
check_interval: Duration::from_secs(20 * 60), // 20 minutes
triggers: vec![
SleeptimeTrigger {
name: "hyperfocus_check".into(),
condition: TriggerCondition::TimeElapsed {
duration: Duration::from_secs(90 * 60),
},
priority: TriggerPriority::High,
},
SleeptimeTrigger {
name: "constellation_sync".into(),
condition: TriggerCondition::ConstellationActivity {
message_threshold: 20,
time_threshold: Duration::from_secs(60 * 60),
},
priority: TriggerPriority::Medium,
},
],
intervention_agent_id: Some(pattern_id),
}pub enum GroupMemberRole {
Regular,
Supervisor,
Observer, // Receives messages but doesn't respond
Specialist { domain: String },
}Groups emit events during processing:
pub enum GroupResponseEvent {
Started { group_id, pattern, agent_count },
AgentStarted { agent_id, agent_name, role },
TextChunk { agent_id, text, is_final },
ToolCallStarted { agent_id, call_id, fn_name, args },
ToolCallCompleted { agent_id, call_id, result },
AgentCompleted { agent_id, agent_name, message_id },
Complete { group_id, pattern, execution_time, agent_responses, state_changes },
Error { agent_id, message, recoverable },
}AgentGroup {
name: "Main Support".into(),
coordination_pattern: CoordinationPattern::Dynamic {
selector_name: "capability".into(),
selector_config: HashMap::new(),
},
members: vec![pattern, entropy, flux, momentum, anchor, archive],
}AgentGroup {
name: "Crisis Response".into(),
coordination_pattern: CoordinationPattern::RoundRobin {
current_index: 0,
skip_unavailable: true,
},
members: vec![pattern, momentum, anchor],
}AgentGroup {
name: "Planning".into(),
coordination_pattern: CoordinationPattern::Supervisor {
leader_id: entropy_id,
delegation_rules: DelegationRules::default(),
},
members: vec![entropy, flux, pattern],
}AgentGroup {
name: "Memory Consolidation".into(),
coordination_pattern: CoordinationPattern::Sleeptime {
check_interval: Duration::from_secs(20 * 60),
triggers: vec![/* ... */],
intervention_agent_id: Some(archive_id),
},
members: vec![pattern, archive],
}The same agent can belong to multiple groups with different coordination styles:
Pattern Agent
├── Main Support (Dynamic)
├── Crisis Response (RoundRobin)
├── Planning (Supervisor - as member)
└── Sleeptime (Sleeptime - as intervener)
Entropy Agent
├── Main Support (Dynamic)
└── Planning (Supervisor - as leader)
Archive Agent
├── Main Support (Dynamic)
└── Sleeptime (Sleeptime - as leader)
Benefits:
- Context-specific coordination styles
- Cost optimization (simpler models for sleeptime)
- Focused agent participation per context
- Easy to experiment with different configurations
Each agent maintains domain-specific observations:
// Entropy observes task patterns
memory.insert_archival(
entropy_id,
"Tasks labeled 'quick fix' average 3.2x estimated time",
Some(json!({"domain": "task_patterns"})),
).await?;Archive agent processes accumulated insights during sleeptime:
// Sleeptime trigger fires
// Archive reads recent observations from all agents
let all_observations = memory.search_all("observations last 24h", options).await?;
// Consolidates into meta-patterns
memory.insert_archival(
archive_id,
"Cross-agent pattern: User productivity drops 40% after 2pm meetings",
Some(json!({"domain": "meta_patterns", "confidence": 0.85})),
).await?;
// Updates shared blocks with key insights
memory.update_block_text(
pattern_id,
"active_context",
"Note: Schedule creative work before meetings today",
).await?;Partner interaction
↓
Core memory updated (immediate context)
↓
Agent observes and processes
↓
Insights stored to archival memory
↓
Sleeptime trigger fires
↓
Archive consolidates patterns
↓
Key insights → shared blocks
↓
All agents see updated context
- Routine monitoring: Lightweight checks (rules-based or small model)
- Intervention: Full model for complex interactions
Sleeptime patterns only invoke expensive models when triggers fire:
TriggerCondition::ThresholdExceeded {
metric: "stress_level".into(),
threshold: 8.0,
}Queue processing accumulates messages between expensive model calls:
let processor = QueueProcessor::new(config, runtime);
processor.start(); // Polls queue, batches notifications# List groups
pattern group list
# Create group (interactive TUI builder)
pattern group create
# Add member
pattern group add member "Planning" Flux --role specialist
# View group status
pattern group status "Planning"
# Export group configuration
pattern group export "Planning" -o planning.toml// Route message to appropriate group
let group_id = select_group(&message, &user_state);
let events = group_manager.route_message(&group, &agents, message).await?;
// Stream responses back to Discord
while let Some(event) = events.next().await {
match event {
GroupResponseEvent::TextChunk { agent_id, text, .. } => {
discord.send_typing(&channel_id).await?;
discord.send_message(&channel_id, &text).await?;
}
// ...
}
}Groups can be exposed as MCP tools:
{
"name": "route_to_group",
"description": "Route a message through an agent group",
"inputSchema": {
"type": "object",
"properties": {
"group_id": { "type": "string" },
"message": { "type": "string" }
}
}
}