Scaffolds Overview
Scaffolds are Egregore’s system for persistent agent memory and capabilities. They’re dynamic components that automatically render into context, provide tool operations, and maintain state across interactions.
What are Scaffolds?
Think of scaffolds as persistent plugins for your agent that:
Maintain state - Remember information across conversations
Provide tools - Expose operations the agent can use
React to changes - Automatically update when context changes
Communicate - Share state through formal IPC system
Scaffolds are like “smart memory” - they observe context, maintain persistent state, and provide capabilities the agent can access.
The Scaffold Pattern
Traditional approaches require manual state management:
# Manual state management - fragile
notes = []
def add_note ( note : str ):
global notes
notes.append(note)
# Manually insert into context
agent.context.pact_insert( "d0, 1, 0" , TextContent( f "Notes: { notes } " ))
Scaffolds solve this with automatic state management:
# Scaffold - automatic and persistent
agent = Agent( provider = "openai:gpt-4" , enable_scaffolds = True )
agent.call( "Remember: user prefers Python" )
# InternalNotesScaffold automatically:
# 1. Captures the note
# 2. Stores it in persistent state
# 3. Renders it into context
# 4. Provides tools for retrieval
# Later...
agent.call( "What do I prefer?" )
# Agent has access to all notes automatically
Built-in Scaffolds
Egregore includes three powerful built-in scaffolds:
1. InternalNotesScaffold
Automatic note-taking and memory:
agent = Agent( provider = "openai:gpt-4" , enable_scaffolds = True )
# Agent automatically captures important information
agent.call( "My API key is sk-abc123" )
agent.call( "I prefer dark mode" )
agent.call( "My email is alice@example.com" )
# Access notes
notes = agent.scaffolds[ "notes" ]
print (notes.state.notes)
# ["API key: sk-abc123", "Preference: dark mode", "Email: alice@example.com"]
# Agent can recall notes
agent.call( "What's my email?" )
# "Your email is alice@example.com"
Features:
Automatic note capture
Persistent storage across sessions
Search and retrieval operations
Category organization
2. FileManager
Track file operations and maintain file system context:
agent = Agent( provider = "openai:gpt-4" , enable_scaffolds = True )
# Scaffold tracks file operations
agent.call( "Read config.json" )
agent.call( "Write to output.txt" )
# Access file history
files = agent.scaffolds[ "files" ]
print (files.state.recent_files)
# ["config.json", "output.txt"]
# Scaffold provides context about files
agent.call( "What files did I work with?" )
# "You read config.json and wrote to output.txt"
Features:
File operation tracking
Working directory awareness
Recent file history
File relationship mapping
3. ShellScaffold
Command execution history and environment tracking:
agent = Agent( provider = "openai:gpt-4" , enable_scaffolds = True )
# Scaffold tracks commands
agent.call( "Run: npm install" )
agent.call( "Execute: pytest" )
# Access command history
shell = agent.scaffolds[ "shell" ]
print (shell.state.command_history)
# [("npm install", "success"), ("pytest", "success")]
# Scaffold provides execution context
agent.call( "What commands did I run?" )
# "You ran npm install and pytest, both successful"
Features:
Command history tracking
Exit code monitoring
Environment variable awareness
Working directory tracking
Learn More Complete guide to built-in scaffolds
Core Concepts
Scaffold State
Each scaffold maintains persistent state:
from egregore.core.context_scaffolds.base import BaseContextScaffold
from pydantic import BaseModel
class TaskState ( BaseModel ):
tasks: list[ str ] = []
completed: list[ str ] = []
class TaskScaffold ( BaseContextScaffold ):
scaffold_type = "tasks"
state_model = TaskState
def render ( self ):
# Render current state into context
return TextContent(
content = f "Active: { len ( self .state.tasks) } , Completed: { len ( self .state.completed) } " ,
key = "task_summary"
)
State characteristics:
Type-safe with Pydantic models
Persistent across interactions
Accessible to other scaffolds
Survives agent restarts (if serialized)
Operations
Scaffolds expose operations as tools:
class TaskScaffold ( BaseContextScaffold ):
scaffold_type = "tasks"
@operation
def add_task ( self , task : str ) -> str :
"""Add a new task."""
self .state.tasks.append(task)
return f "Added: { task } "
@operation
def complete_task ( self , task : str ) -> str :
"""Mark task as completed."""
self .state.tasks.remove(task)
self .state.completed.append(task)
return f "Completed: { task } "
# Agent can use scaffold operations
agent = Agent( provider = "openai:gpt-4" , scaffolds = [TaskScaffold])
agent.call( "Add task: write documentation" )
# Scaffold's add_task() called automatically
Operation features:
Automatic tool generation
Type safety from annotations
Return values sent to agent
State changes trigger re-rendering
Learn More Complete @operation decorator documentation
Reactive Rendering
Scaffolds automatically re-render when context changes:
class SmartScaffold ( BaseContextScaffold ):
scaffold_type = "smart"
_reactive = True # Default: True
def render ( self ):
# Called automatically when context changes
episode = self .agent.context.current_episode
return TextContent(
content = f "Current episode: { episode } " ,
key = "episode_tracker"
)
def should_rerender ( self , ctx : ContextExecContext) -> bool :
# Optional: control when to re-render
return ctx.operation_type == "insert" # Only on insertions
Reactive features:
Automatic re-rendering on context changes
Selective re-rendering via should_rerender()
Minimal performance overhead
All scaffolds reactive by default
Scaffold IPC
Scaffolds communicate through formal IPC:
class AnalyzerScaffold ( BaseContextScaffold ):
scaffold_type = "analyzer"
def render ( self ):
# Read state from another scaffold
notes = self .agent.scaffolds[ "notes" ]
note_count = len (notes.state.notes)
# Write state for other scaffolds
self .agent.state.set( "analysis_status" , "active" , source = "analyzer" )
return TextContent(
content = f "Analyzed { note_count } notes" ,
key = "analysis"
)
Learn More Complete scaffold IPC documentation
Scaffold Lifecycle
Registration
Scaffolds are registered when creating an agent:
# Built-in scaffolds
agent = Agent( provider = "openai:gpt-4" , enable_scaffolds = True )
# Custom scaffolds
agent = Agent(
provider = "openai:gpt-4" ,
scaffolds = [TaskScaffold, NotesScaffold]
)
# Access scaffolds
task_scaffold = agent.scaffolds[ "tasks" ]
notes_scaffold = agent.scaffolds[ "notes" ]
Initialization
Scaffolds initialize when first accessed:
class InitScaffold ( BaseContextScaffold ):
scaffold_type = "init"
def __init__ ( self , agent ):
super (). __init__ (agent)
# Initialize state
self .data = []
# Setup connections
self .setup_monitoring()
Rendering
Scaffolds render into context automatically:
class StatusScaffold ( BaseContextScaffold ):
scaffold_type = "status"
def render ( self ):
# Return None to skip rendering
if not self .should_render():
return None
# Return component to render
return TextContent(
content = "Status: active" ,
key = "status" ,
ttl = 1 , # Expires after 1 turn
cadence = 1 # Re-renders each turn (sticky)
)
Rendering triggers:
Initial agent creation
Context changes (if reactive)
Manual scaffold.render() call
State changes via operations
State Persistence
Scaffold state can be serialized:
import json
# Save scaffold state
state_data = agent.scaffolds[ "tasks" ].state.model_dump()
with open ( "task_state.json" , "w" ) as f:
json.dump(state_data, f)
# Restore scaffold state
with open ( "task_state.json" , "r" ) as f:
state_data = json.load(f)
task_scaffold = agent.scaffolds[ "tasks" ]
task_scaffold.state = TaskState( ** state_data)
Use Cases
1. User Preferences
class PreferencesScaffold ( BaseContextScaffold ):
scaffold_type = "preferences"
class StateModel ( BaseModel ):
theme: str = "light"
language: str = "en"
notifications: bool = True
@operation
def set_preference ( self , key : str , value : str ) -> str :
"""Update a preference."""
setattr ( self .state, key, value)
return f "Set { key } to { value } "
def render ( self ):
prefs = [ f " { k } : { v } " for k, v in self .state.dict().items()]
return TextContent(
content = f "Preferences: \n " + " \n " .join(prefs),
key = "user_preferences"
)
agent = Agent( provider = "openai:gpt-4" , scaffolds = [PreferencesScaffold])
agent.call( "Set my theme to dark" )
agent.call( "Enable notifications" )
2. Task Management
class TaskScaffold ( BaseContextScaffold ):
scaffold_type = "tasks"
class StateModel ( BaseModel ):
tasks: list[ dict ] = []
@operation
def add_task ( self , title : str , priority : str = "medium" ) -> str :
"""Add a task."""
task = { "title" : title, "priority" : priority, "status" : "pending" }
self .state.tasks.append(task)
return f "Added task: { title } "
@operation
def complete_task ( self , title : str ) -> str :
"""Complete a task."""
for task in self .state.tasks:
if task[ "title" ] == title:
task[ "status" ] = "completed"
return f "Completed: { title } "
return f "Task not found: { title } "
def render ( self ):
pending = [t for t in self .state.tasks if t[ "status" ] == "pending" ]
return TextContent(
content = f " { len (pending) } pending tasks" ,
key = "task_count"
)
3. Context Summarization
class SummaryScaffold ( BaseContextScaffold ):
scaffold_type = "summary"
def render ( self ):
# Analyze recent messages
thread = self .agent.thread.current
message_count = len (thread.all_messages)
# Track topics
topics = self .extract_topics(thread)
return TextContent(
content = f "Conversation summary: \n "
f "Messages: { message_count } \n "
f "Topics: { ', ' .join(topics) } " ,
key = "conversation_summary" ,
ttl = 5 # Update every 5 turns
)
def extract_topics ( self , thread ):
# Topic extraction logic
return [ "python" , "documentation" , "AI" ]
4. Memory Consolidation
class MemoryScaffold ( BaseContextScaffold ):
scaffold_type = "memory"
class StateModel ( BaseModel ):
short_term: list[ str ] = []
long_term: list[ str ] = []
def render ( self ):
# Consolidate short-term to long-term
if len ( self .state.short_term) > 10 :
summary = self .consolidate( self .state.short_term)
self .state.long_term.append(summary)
self .state.short_term.clear()
return TextContent(
content = f "Memories: { len ( self .state.long_term) } consolidated" ,
key = "memory_status"
)
def consolidate ( self , memories : list[ str ]) -> str :
# Use AI to summarize
summary_agent = Agent( provider = "openai:gpt-4" )
return summary_agent.call(
f "Summarize these memories: \n " + " \n " .join(memories)
)
Best Practices
Type-safe state with validation: from pydantic import BaseModel, Field
class StateModel ( BaseModel ):
count: int = Field( ge = 0 ) # Non-negative
items: list[ str ] = []
enabled: bool = True
class MyScaffold ( BaseContextScaffold ):
state_model = StateModel
Each scaffold should have a single responsibility: # Good: Focused scaffolds
class TaskScaffold ( BaseContextScaffold ): ...
class NotesScaffold ( BaseContextScaffold ): ...
# Bad: Kitchen sink scaffold
class EverythingScaffold ( BaseContextScaffold ):
# tasks, notes, preferences, files, etc. - too much!
Use operations for mutations
Expose state changes as operations: class ScaffoldWithOps ( BaseContextScaffold ):
@operation
def add_item ( self , item : str ) -> str :
"""Add item - exposed as tool."""
self .state.items.append(item)
return f "Added: { item } "
# Don't: Modify state without @operation
# Agent won't be able to use it
Control rendering frequency
Use TTL and selective re-rendering: def render ( self ):
return TextContent(
content = self .generate_summary(),
key = "summary" ,
ttl = 10 , # Only re-render every 10 turns
cadence = 10 # Sticky behavior
)
def should_rerender ( self , ctx ):
# Only re-render on important changes
return ctx.operation_type == "insert"
Rendering Overhead
Scaffolds add minimal overhead:
# Reactive scaffolds: ~5% overhead with 5+ scaffolds
agent = Agent(
provider = "openai:gpt-4" ,
scaffolds = [S1, S2, S3, S4, S5] # Minimal impact
)
# Disable reactivity if needed
class NonReactiveScaffold ( BaseContextScaffold ):
_reactive = False # No automatic re-rendering
State Size
Keep state manageable:
# Good: Bounded state
class BoundedScaffold ( BaseContextScaffold ):
def render ( self ):
# Keep only recent items
if len ( self .state.items) > 100 :
self .state.items = self .state.items[ - 100 :]
# Bad: Unbounded growth
# State grows forever - memory issues
Operation Complexity
Keep operations fast:
# Good: Fast operations
@operation
def add_item ( self , item : str ) -> str :
self .state.items.append(item) # O(1)
return "Added"
# Bad: Slow operations
@operation
def analyze_all ( self ) -> str :
# Expensive AI call - blocks agent
result = expensive_analysis( self .state.items)
return result # Consider async or background processing
What’s Next?