Blackboards & Interfaces¶
The blackboard pattern is central to Mycorrhizal, enabling shared state management across all DSLs. This guide covers both basic blackboard usage and advanced interface-based access control.
Table of Contents¶
Basic Blackboards¶
What is a Blackboard?¶
A blackboard is a shared data structure that all components (states, transitions, tree nodes) can read from and write to. In Mycorrhizal, blackboards are implemented as Pydantic BaseModel classes.
Defining a Blackboard¶
from pydantic import BaseModel
from typing import Optional
class GameContext(BaseModel):
player_health: int = 100
enemy_count: int = 0
current_room: str = "entrance"
has_key: bool = False
Using in Rhizomorph (Behavior Trees)¶
from mycorrhizal.rhizomorph.core import bt, Status
@bt.tree
class GameAI:
@bt.condition
def is_low_health(bb: GameContext) -> bool:
return bb.player_health < 30
@bt.action
async def heal(bb: GameContext) -> Status:
bb.player_health = min(100, bb.player_health + 20)
return Status.SUCCESS
Using in Septum (State Machines)¶
from mycorrhizal.septum.core import septum, StateMachine
@septum.state
class GameState:
@septum.on_state
async def on_state(ctx: GameContext):
if ctx.player_health <= 0:
print("Game Over")
# ...
Using in Hypha (Petri Nets)¶
from mycorrhizal.hypha.core import pn
@pn.net
class GameNet:
@pn.place(type=pn.PlaceType.BOOLEAN)
def player_alive(bb: GameContext):
return bb.player_health > 0
Type Safety¶
Pydantic provides automatic type validation:
class GameContext(BaseModel):
player_health: int = 100 # Must be an int
# This will raise a validation error
try:
bb = GameContext(player_health="invalid")
except ValidationError as e:
print(f"Validation error: {e}")
Default Values¶
Use default values to initialize your blackboard:
Nested Models¶
Blackboards can contain nested models for complex state:
class Position(BaseModel):
x: float
y: float
z: float = 0.0
class Robot(BaseModel):
id: str
position: Position
battery: int = 100
class WorldContext(BaseModel):
robots: dict[str, Robot] = {}
obstacles: list[Position] = []
Shared Blackboards Across DSLs¶
You can share the same blackboard between different DSLs:
# Create a shared blackboard
bb = RobotContext()
# Use in a behavior tree
bt_runner = BTRunner(tree=tree, blackboard=bb)
# Use in a Petri net
pn_runner = PNRunner(net=net, blackboard=bb)
Advanced: Computed Properties¶
Use Pydantic's computed_field for derived values:
from pydantic import BaseModel, computed_field
class Character(BaseModel):
health: int = 100
max_health: int = 100
@computed_field
@property
def health_percentage(self) -> float:
return (self.health / self.max_health) * 100
Blackboard Interfaces¶
While basic blackboards work well for simple systems, larger projects benefit from interfaces - type-safe, constrained views of blackboard state that enforce access control at both type-level and runtime.
Why Use Interfaces?¶
Interfaces provide several key benefits:
- Access Control - Prevent accidental modification of configuration or internal state
- Type Safety - Type checkers can verify only appropriate fields are accessed
- Better Testing - Mock interfaces instead of full blackboards
- Clearer Contracts - Function signatures show exactly what state they need
- Safer Composition - Components can only access relevant fields
Defining Interfaces¶
Use the @blackboard_interface decorator with Annotated types to define interfaces:
from typing import Annotated
from mycorrhizal.common.interface_builder import blackboard_interface, readonly, readwrite
@blackboard_interface
class GameInterface:
# Readonly configuration - can read but not write
max_health: Annotated[int, readonly]
difficulty: Annotated[str, readonly] = "normal"
# Readwrite state - can both read and write
current_health: Annotated[int, readwrite]
player_name: Annotated[str, readwrite]
# Internal fields (not annotated) are hidden
# _internal_debug: str # Would be excluded from interface
Access Control Markers:
Annotated[type, readonly]- Field can be read but not modifiedAnnotated[type, readwrite]- Field can be both read and modified- Unannotated fields - Hidden from the interface
- Private fields (starting with
_) - Automatically excluded
Creating Views¶
Once you have an interface, create a view - a runtime wrapper that enforces the interface constraints:
from mycorrhizal.common.wrappers import create_view_from_protocol, AccessControlError
# Your blackboard (Pydantic model)
class GameBlackboard(BaseModel):
max_health: int = 100
current_health: int = 100
player_name: str = "Player"
difficulty: str = "normal"
_internal_debug: bool = False
# Create blackboard instance
bb = GameBlackboard()
# Create a view that enforces GameInterface constraints
game_view = create_view_from_protocol(bb, GameInterface, readonly_fields={'max_health', 'difficulty'})
# Reading works for all fields
print(game_view.max_health) # ✓ OK
print(game_view.current_health) # ✓ OK
# Writing to readwrite fields works
game_view.current_health = 50 # ✓ OK
# Writing to readonly fields is prevented
try:
game_view.max_health = 200 # ✗ Raises AccessControlError
except AccessControlError as e:
print(f"Protected: {e}")
# Accessing internal fields is prevented
try:
_ = game_view._internal_debug # ✗ Raises AttributeError
except AttributeError:
print("Internal field hidden")
Wrapper Classes¶
Mycorrhizal provides several wrapper classes for different access patterns:
ReadOnlyView¶
Prevents all modifications:
from mycorrhizal.common.wrappers import ReadOnlyView
# View that only allows reading configuration
config_view = ReadOnlyView(bb, {'max_health', 'difficulty'})
print(config_view.max_health) # ✓ OK
config_view.max_health = 200 # ✗ Raises AccessControlError
ConstrainedView¶
Allows selective access with mixed permissions:
from mycorrhizal.common.wrappers import ConstrainedView
# Some fields readonly, some readwrite
view = ConstrainedView(
bb,
allowed_fields={'max_health', 'current_health'},
readonly_fields={'max_health'}
)
view.current_health = 50 # ✓ OK (readwrite)
view.max_health = 200 # ✗ Raises (readonly)
CompositeView¶
Combines multiple views:
from mycorrhizal.common.wrappers import CompositeView
# Create multiple views
readonly_view = ReadOnlyView(bb, {'max_health'})
state_view = ConstrainedView(bb, {'current_health'}, readonly_fields=set())
# Combine into single interface
composite = CompositeView.combine([readonly_view, state_view])
print(composite.max_health) # ✓ From readonly_view
composite.current_health = 50 # ✓ Through state_view
composite.max_health = 200 # ✗ Still readonly
View Factory¶
The View() function provides a clean, type-safe API:
from mycorrhizal.common.wrappers import View
# Type-safe view creation
view: GameInterface = View(bb, GameInterface)
# Type checker knows view has max_health and current_health
print(view.max_health)
Interface Metadata¶
Interfaces store metadata for introspection:
@blackboard_interface
class TaskInterface:
max_tasks: Annotated[int, readonly]
completed_tasks: Annotated[int, readwrite]
failed_tasks: Annotated[int, readwrite]
# Check interface metadata
print(TaskInterface._readonly_fields) # {'max_tasks'}
print(TaskInterface._readwrite_fields) # {'completed_tasks', 'failed_tasks'}
# Validate that a blackboard implements an interface
# Use isinstance() directly with runtime_checkable protocols
is_valid = isinstance(TaskBlackboard, TaskInterface)
DSL Integration with Interfaces¶
All three DSLs (Hypha, Rhizomorph, Septum) support interfaces through their runners.
Rhizomorph (Behavior Trees)¶
Pass a view instead of the full blackboard:
from mycorrhizal.rhizomorph.core import BTRunner, bt, Status
from mycorrhizal.common.wrappers import create_view_from_protocol
# Create blackboard and constrained view
bb = GameBlackboard()
game_view = create_view_from_protocol(bb, GameInterface, {'max_health'})
# Use view in behavior tree
@bt.tree
class GameAI:
@bt.condition
def is_low_health(bb: GameInterface) -> bool: # Type hint shows interface
return bb.current_health < bb.max_health * 0.3
@bt.action
async def heal(bb: GameInterface) -> Status:
bb.current_health = min(bb.max_health, bb.current_health + 20)
return Status.SUCCESS
# Create runner with view
runner = BTRunner(tree=GameAI, blackboard=game_view)
Septum (State Machines)¶
from mycorrhizal.septum.core import StateMachine, septum
# Create view
game_view = create_view_from_protocol(bb, GameInterface, {'max_health'})
@septum.state
class PlayingState:
@septum.on_state
async def on_state(ctx: GameInterface): # Interface type hint
if ctx.current_health <= 0:
print("Game Over")
# ctx.max_health = 200 # ✗ Would be caught by type checker
# Create FSM with view
fsm = StateMachine(initial_state=PlayingState, blackboard=game_view)
Hypha (Petri Nets)¶
from mycorrhizal.hypha.core import Runner as PNRunner, pn
# Create view
game_view = create_view_from_protocol(bb, GameInterface, {'max_health'})
@pn.net
class GameNet:
@pn.place(type=pn.PlaceType.BOOLEAN)
def is_alive(bb: GameInterface): # Interface type hint
return bb.current_health > 0
@pn.transition()
async def take_damage(consumed, bb: GameInterface, timebase):
bb.current_health = max(0, bb.current_health - 10)
# Create runner with view
runner = PNRunner(net=GameNet, blackboard=game_view)
Multiple Interfaces per Blackboard¶
Create different interfaces for different access levels:
@blackboard_interface
class ReadOnlyConfig:
"""Read-only access to configuration"""
max_health: Annotated[int, readonly]
difficulty: Annotated[str, readonly]
@blackboard_interface
class GameStateAccess:
"""Full access to game state"""
max_health: Annotated[int, readonly]
current_health: Annotated[int, readwrite]
player_name: Annotated[str, readwrite]
@blackboard_interface
class AdminAccess:
"""Administrative access with internal fields"""
max_health: Annotated[int, readwrite] # Can modify config
current_health: Annotated[int, readwrite]
_debug_mode: Annotated[bool, readwrite] # Can access internals
# Use appropriate interface for each component
readonly_view = create_view_from_protocol(bb, ReadOnlyConfig, {'max_health', 'difficulty'})
player_view = create_view_from_protocol(bb, GameStateAccess, {'max_health'})
admin_view = create_view_from_protocol(bb, AdminAccess, set())
Best Practices¶
General¶
- Use type hints - Enable better IDE support and validation
- Document important fields - Help other developers understand the state
- Avoid overly nested models - Keep the structure relatively flat
- Use default values - Make initialization easier
- Consider immutability - For complex concurrent systems
Using Interfaces¶
- Use interfaces for large systems - Prevent accidental modification
- Make configuration read-only - Use
readonlyfor fields that shouldn't change - Hide internal fields - Don't annotate implementation details
- Create focused interfaces - One interface per component role
- Validate at boundaries - Create views when passing between components
- Leverage type checking - Use interface type hints in function signatures
Common Patterns¶
Configuration Blackboard¶
class SystemConfig(BaseModel):
max_workers: int = 4
timeout: float = 30.0
log_level: str = "INFO"
@blackboard_interface
class ConfigView:
"""Read-only configuration access"""
max_workers: Annotated[int, readonly]
timeout: Annotated[float, readonly]
log_level: Annotated[str, readonly]
State Tracking Blackboard¶
class ProcessState(BaseModel):
current_step: str = "init"
steps_completed: list[str] = []
error_count: int = 0
last_error: Optional[str] = None
@blackboard_interface
class StateAccess:
"""Access to process state"""
current_step: Annotated[str, readwrite]
steps_completed: Annotated[list[str], readwrite]
error_count: Annotated[int, readwrite]
last_error: Annotated[Optional[str], readwrite]
Resource Management Blackboard¶
class ResourceManager(BaseModel):
total_memory: int = 1024
allocated_memory: int = 0
active_connections: int = 0
@blackboard_interface
class ResourceConsumer:
"""Consumer view - can allocate but not change total"""
total_memory: Annotated[int, readonly]
allocated_memory: Annotated[int, readwrite]
active_connections: Annotated[int, readwrite]
@blackboard_interface
class ResourceAdmin:
"""Admin view - can modify total resources"""
total_memory: Annotated[int, readwrite]
allocated_memory: Annotated[int, readwrite]
active_connections: Annotated[int, readwrite]
See Also¶
- Timebases - Time abstraction for blackboards
- Composition - Combining systems with shared state
- API Reference - Complete API documentation for interfaces and wrappers