Theater Documentation
Welcome to the official documentation for Theater, a WebAssembly actor system designed for building secure, reliable, and transparent AI agent infrastructure.
How to Use This Documentation
This documentation is organized into sections:
- Introduction: Understand why Theater exists and the challenges it addresses in AI agent systems
- Core Concepts: Learn what Theater is and its fundamental principles
- Use Cases: Explore how Theater can be applied to build robust AI agent systems
- User Guide: Find practical information on using Theater in your projects
- Development: Learn how to build agents and extend Theater's functionality
- Services: Explore the built-in services and handler systems
- API Reference: Access detailed API documentation
The Three Pillars of Theater
Theater is built on three foundational pillars that make it ideal for AI agent infrastructure:
- WebAssembly Components & Sandboxing: Security boundaries and capability controls for agent containment
- Actor Model & Supervision: Isolated agents, message-passing communication, and fault tolerance through supervision
- Traceability & Verification: Comprehensive tracking of all agent actions for transparency and debugging
Understanding these pillars provides the foundation for building effective AI agent systems with Theater.
Quick Links
- Why Theater? - Understand the challenges of AI agent systems that Theater solves
- Core Concepts Overview - Learn the fundamental ideas behind Theater
- Building AI Agent Systems - Explore how to build robust agent architectures
- CLI Reference - Command-line interface documentation
- Building Your First Agent - Start creating with Theater
Theater for AI Agents
Theater provides the infrastructure needed to build trustworthy AI agent systems:
- Secure Execution: Run agents in sandboxed environments with precise capability controls
- Agent Orchestration: Create hierarchies of specialized agents that work together
- Complete Traceability: Capture and analyze every action taken by agents
- Failure Resilience: Automatically recover from agent failures through supervision
- Transparent Operation: Build trust through comprehensive visibility into agent behavior
Additional Resources
- GitHub Repository - Source code and issue tracking
This book is continuously updated as Theater evolves. If you find any issues or have suggestions for improvement, please submit them through our GitHub repository.
Overview
Theater is a WebAssembly actor system designed for a world where code may never see human review. It provides an environment where components can interact safely, failures can be contained, and the entire system can be traced and debugged with unprecedented clarity.
A New Era of Software Development
We stand at the beginning of a transformation in how software is written. Large Language Models are already generating significant amounts of code, and this trend will only accelerate. Soon, a substantial portion of the code touching users may never have been seen by human eyes.
This shift presents both opportunities and challenges. On one hand, our software can become more adaptable and flexible, and allow us to tackle problems that were previously too complex or time-consuming. On the other hand, the fundamental assumptions that our software ecosystem is built upon—human review, intentional design, and careful testing—are being upended.
The Three Pillars of Theater
Theater builds trust into the structure of the software system itself through three foundational pillars:
-
WebAssembly Components & Sandboxing provide security boundaries and deterministic execution, ensuring that code operates within well-defined constraints.
-
Actor Model & Supervision implements an Erlang-style actor system with hierarchical supervision, creating isolation between components and facilitating recovery from failures.
-
Traceability & Verification tracks all information that enters or leaves the WebAssembly sandbox, creating a comprehensive audit trail for debugging and verification.
These three pillars work together to create a system that is secure, resilient, and transparent, addressing the unique challenges of running AI-generated code at scale.
Documentation Structure
This book is organized to provide a clear learning path:
- Introduction: Why Theater exists and the problems it solves
- Core Concepts: What Theater is and its fundamental principles
- Architecture: How Theater works internally
- User Guide: Practical information for using Theater
- Development: Building and extending Theater components
- Services: Built-in capabilities and handler systems
Each section builds on the previous ones, providing a progressively deeper understanding of the Theater system.
Who Is Theater For?
Theater is currently an experimental project for:
- Developers exploring new approaches to software reliability
- Researchers interested in secure execution of untrusted code
- Early adopters ready to help shape the future of AI-aware systems
It is not intended for production use at this time but provides a glimpse into a future where systems are designed with AI-generated code in mind.
About This Book
This book serves as a friendly introduction to the Theater system. It is meant for programmers new to Theater with existing programming experience. For a more in-depth and precise understanding of the system and its component parts, please refer to the API Reference.
Why Theater?
The Challenge of Trust in the AI Agent Era
As AI systems become more capable, we're rapidly moving towards a world of autonomous AI agents - software entities that can perform complex tasks, make decisions, and interact with digital systems on our behalf. These agents promise enormous benefits in productivity and automation, but they also present significant challenges that our current software infrastructure isn't designed to address.
These challenges include:
- Security Boundaries: How do we ensure agents only access systems and data they're explicitly allowed to?
- Visibility and Transparency: How do we observe, audit, and understand what agents are doing?
- Coordination and Cooperation: How do we enable multiple specialized agents to work together safely?
- Failure Management: How do we handle agents that encounter errors or behave in unexpected ways?
- Trust and Verification: How do we build confidence in agent-based systems, especially in critical applications?
These aren't merely theoretical concerns. They represent real, practical challenges that must be solved before AI agents can be deployed widely and safely in production environments.
Shifting Trust from Agents to Infrastructure
The traditional approach to software reliability has focused on extensive testing and validation of individual components. This approach assumes that once software passes quality checks, it can be trusted to behave correctly.
With AI agents, this assumption becomes problematic. Agents may exhibit emergent behaviors, make unexpected decisions, or encounter edge cases that weren't anticipated during development. Their behavior can be complex and sometimes opaque, making traditional validation insufficient.
Theater takes a different approach. Rather than assuming all agents will behave perfectly, Theater shifts trust to the infrastructure itself. By providing strong guarantees at the system level, Theater creates an environment where even agents with unpredictable behaviors can operate safely.
This shift parallels other advancements in computing history:
- Virtual machines shifted trust from applications to hypervisors
- Containers shifted trust from monoliths to orchestration platforms
- Serverless shifted trust from server management to cloud providers
Theater continues this evolution by shifting trust from agent behavior to system-level guarantees.
The Three Pillars of Theater for AI Agents
Theater uses three key pillars to provide guarantees about agents running in the system:
1. WebAssembly Components & Sandboxing
Theater uses WebAssembly Components as its foundation, providing:
- Strict Capability Boundaries: Agents only have access to capabilities explicitly granted to them
- Resource Isolation: Each agent runs in its own sandbox, preventing direct access to the host system or other agents
- Deterministic Execution: The same inputs always produce the same outputs, making behavior predictable and reproducible
- Language Agnosticism: Agents can be implemented in any language that compiles to WebAssembly
2. Actor Model & Supervision
Taking inspiration from Erlang/OTP, Theater implements a comprehensive actor system:
- Agent-to-Agent Communication: All communication happens through explicit message passing
- Hierarchical Supervision: Parent agents monitor children and can restart them upon failure
- Failure Isolation: Problems in one agent don't affect siblings or unrelated parts of the system
- Specialized Roles: Agents can be designed with specific capabilities and responsibilities, forming natural hierarchies
3. Traceability & Verification
Theater tracks every action that agents take:
- Event Chain: All agent actions are recorded in a verifiable chain
- Complete Auditability: Every decision and action can be traced back to its causes
- Deterministic Replay: Any sequence of events can be replayed exactly for debugging
- Explainable Behavior: The complete history of agent interactions is available for inspection and analysis
Building for an AI Agent Ecosystem
By providing a structured environment with strong system-level guarantees, Theater enables developers to build more trustworthy agent systems. This approach is particularly valuable as we move into an era where autonomous agents become increasingly important in our software landscape.
Theater doesn't try to make agent behavior perfectly predictable. Instead, it creates an environment where:
- Agents can only access what they're explicitly permitted to
- Every agent action is recorded and auditable
- Failed agents can be automatically restarted or replaced
- Complex tasks can be broken down across multiple specialized agents
- The entire system behavior is transparent and verifiable
Theater provides the infrastructure necessary to deploy AI agents with confidence, knowing that no matter how sophisticated the agents become, the system provides guardrails to ensure they operate safely and reliably.
In the following chapters, we'll explore how Theater implements these principles in practice, starting with the core concepts that form the foundation of the system.
Core Concepts
Theater is built on three fundamental pillars that work together to create a system that is secure, reliable, and transparent. This section explains what Theater is and the key concepts that make it the ideal infrastructure for AI agent systems.
The Three Pillars of Theater
WebAssembly Components & Sandboxing
WebAssembly provides the foundation for Theater's security and capability controls:
- Strong security boundaries through sandboxing
- Deterministic execution for reproducible behavior
- Language-agnostic agent implementation
- Capability-based security model for precise access control
Actor Model & Supervision
The Actor Model enables Theater's approach to agent organization, communication, and fault tolerance:
- Agents as independent, isolated entities
- Message-passing for all agent-to-agent communication
- Private state management for each agent
- Hierarchical supervision for reliable agent systems
Traceability & Verification
Traceability ensures that all agent actions are observable, auditable, and debuggable:
- Event Chain capturing every agent action
- Deterministic replay for verification and debugging
- State management for consistent agent snapshots
- Comprehensive tools for inspection and analysis
How The Pillars Work Together
These three pillars complement each other to create Theater's unique properties for agent systems:
- WebAssembly + Actor Model: Provides secure agents with clear communication patterns
- WebAssembly + Traceability: Enables deterministic replay and verification of agent behavior
- Actor Model + Traceability: Supports failure diagnosis and recovery in complex agent systems
By understanding these core concepts, you'll have a solid foundation for building reliable, secure, and transparent AI agent systems with Theater.
WebAssembly Components & Sandboxing
WebAssembly (Wasm) Components form the foundational pillar for Theater's agent security, isolation, and deterministic behavior. By leveraging the Wasm Component Model, Theater creates sandboxed environments where AI agents can operate predictably and securely, with precise control over their capabilities and access to resources.
The Power of WebAssembly for Secure Agent Execution
WebAssembly was designed with security as a primary goal. It provides several key features that make it ideal for running AI agents:
-
Strong Sandboxing: Each AI agent runs in a completely isolated memory space. Agents cannot access the host system's resources (like files, network, or environment variables) or the memory of other agents unless explicitly granted permission via specific imported functions.
-
Limited Instruction Set: Wasm's instruction set is intentionally minimal and well-defined. This eliminates entire classes of vulnerabilities common in native code execution, making it safer to run autonomous agents.
-
Capability-Based Security: The Wasm Component Model relies on explicit interfaces defined using the WebAssembly Interface Type (WIT) language. Agents declare the capabilities they need (like accessing specific APIs or communicating with other agents), and Theater acts as the gatekeeper, controlling which capabilities are provided to each agent.
This sandboxed, capability-based approach means that even sophisticated AI agents with complex behaviors can operate with strong security guarantees. Theater confines each agent's operations to only the resources and capabilities explicitly granted to them.
Deterministic Execution for Predictable Agent Behavior
A crucial property Theater gains from WebAssembly is deterministic execution. Given the same inputs, an agent implemented as a Wasm component will always produce the same outputs and side effects within the sandbox.
- Well-Defined Semantics: Wasm has rigorously defined behavior, avoiding the ambiguities and undefined behaviors found in many other execution environments.
- Controlled Environment: All interactions with the outside world (beyond pure computation) must go through imported host functions, which Theater controls and monitors.
This determinism is essential for Theater's Traceability & Verification pillar, enabling reliable replay, debugging, and verification of agent behavior.
Language Agnosticism for Flexible Agent Implementation
WebAssembly serves as a portable compilation target for numerous programming languages (Rust, C/C++, Go, AssemblyScript, C#, and more). The Wasm Component Model further enhances this by allowing components written in different languages to interoperate seamlessly.
- Implement Agents in Your Preferred Language: Developers can choose the best language for their specific agent implementation.
- Compose Diverse Capabilities: An agent within Theater might itself be composed of multiple Wasm components, potentially written in different languages, providing specialized functionality.
- Consistent Runtime Behavior: Regardless of the implementation language, the compiled agent behaves predictably within the Theater runtime.
While the Component Model is still evolving across the ecosystem (with Rust having the most mature tooling currently), it provides a powerful, standardized way to build modular and interoperable agent systems.
Interface Definitions with WIT
Theater uses the WebAssembly Interface Type (WIT) language to define the contracts between agents and the system:
- Agent Interfaces: Specifies the functions an agent exposes to be called by Theater or other agents.
- Host Capabilities: Defines the capabilities the Theater runtime provides to agents (e.g., sending messages, accessing external APIs, storing data).
- Message Formats: Describes the structure and types of data exchanged between agents and the host.
These explicit interface definitions ensure clarity about what each agent can do and enforce the capability-based security model.
Benefits for AI Agent Systems
By building upon WebAssembly Components, Theater achieves:
- Strong Isolation: Preventing agents from interfering with each other or the host system.
- Precise Capability Control: Granting agents only the specific access rights they need.
- Execution Determinism: Enabling reliable replay, verification, and debugging of agent behavior.
- Implementation Flexibility: Allowing agents to be developed in various languages.
- Modularity and Composability: Facilitating the creation of complex agent systems from specialized components.
- Portability: Ensuring agents can run consistently across different environments supporting Wasm.
This foundation allows Theater to provide a robust runtime environment for AI agent systems where security, reliability, and transparency are paramount concerns.
Actor Model & Supervision for AI Agents
The Actor Model is the second pillar of Theater, providing a robust framework for organizing, communicating, and managing AI agents. Inspired by systems like Erlang/OTP, Theater implements actors with hierarchical supervision to build resilient and scalable agent architectures.
Agents as Actors: Isolated Units of Intelligence
In Theater, each AI agent is implemented as an actor. Each agent is an independent entity characterized by:
- Private State: An agent maintains its own internal state, which cannot be directly accessed or modified by other agents. This isolation ensures agent autonomy and prevents interference.
- Mailbox: Each agent has a mailbox where incoming messages are queued, enabling asynchronous communication.
- Behavior: An agent defines how it processes incoming messages, potentially changing its state, sending messages to other agents, or taking actions in the external world.
- Identity: Agents have unique identifiers used to address messages to them.
Crucially, in Theater, each agent corresponds to a running WebAssembly Component, benefiting from the security and isolation guarantees provided by Wasm.
Agent Communication via Asynchronous Message Passing
All interaction between agents in Theater occurs exclusively through asynchronous message passing.
- No Shared Memory: Agents do not share memory. To communicate, an agent sends an immutable message to another agent's address.
- Asynchronous & Non-Blocking: When an agent sends a message, it does not wait for the recipient to process it. This allows agents to work concurrently without blocking each other.
- Location Transparency: Agents communicate using addresses, without needing to know the physical location of the recipient agent. (Note: While Theater currently runs agents within a single process, the model allows for future distribution).
- Explicit and Traceable: All interactions are explicit message sends, which are captured by Theater's Traceability system.
This communication style creates clear boundaries between agents, simplifies concurrency management, and provides a natural way to organize complex agent systems.
Agent Isolation: The Core Benefit
The strict isolation provided by the Actor Model (agents having their own state and communicating only via messages) delivers several critical benefits for AI agent systems:
- Fault Isolation: If an agent encounters an error or behaves unexpectedly, the failure is contained within that agent. It does not directly affect the state or operation of other agents in the system.
- Independent Lifecycle: Agents can be started, stopped, restarted, or even upgraded independently without necessarily affecting unrelated parts of the system.
- Capability Containment: Each agent can be granted precisely the capabilities it needs, without sharing those capabilities with other agents.
- State Management: Because state is encapsulated, Theater can manage agent state persistence and recovery more easily. State can potentially be preserved across restarts or upgrades.
Hierarchical Supervision for Reliable Agent Systems
Theater adopts Erlang/OTP's concept of hierarchical supervision to manage agent failures gracefully.
- Supervision Trees: Agents are organized into a tree structure where parent agents supervise their child agents.
- Monitoring: Supervisors monitor the health and behavior of their children.
- Recovery Strategies: When a child agent fails (e.g., crashes due to an unhandled error), its supervisor is notified and decides how to handle the failure based on a defined strategy. Common strategies include:
- Restart: Restart the failed agent, potentially restoring its last known good state.
- Stop: Terminate the failed agent permanently if it's deemed unrecoverable or non-essential.
- Escalate: If the supervisor cannot handle the failure, it can fail itself, escalating the problem to its supervisor.
- Restart Siblings: In some cases, a failure in one agent might require restarting other related agents (siblings).
This structure allows developers to define how the system should react to failures, building self-healing capabilities directly into the agent architecture. Error handling becomes a primary architectural concern, rather than an afterthought.
Agent Patterns with the Actor Model
The Actor Model enables several powerful patterns for AI agent systems:
1. Specialized Agent Teams
Create teams of specialized agents that work together on complex tasks:
CoordinatorAgent
├── ResearchAgent
├── AnalysisAgent
└── ReportGenerationAgent
Each agent focuses on what it does best, communicating results to the next agent in the workflow.
2. Agent Redundancy and Load Balancing
Create multiple instances of the same agent type to handle high workloads or provide redundancy:
RouterAgent
├── WorkerAgent-1
├── WorkerAgent-2
├── WorkerAgent-3
└── WorkerAgent-4
The router distributes work across the workers and can easily spin up more workers as needed.
3. Progressive Agent Specialization
Create hierarchies of increasingly specialized agents:
GeneralCoordinatorAgent
├── ResearchTeamAgent
│ ├── WebSearchAgent
│ ├── AcademicDatabaseAgent
│ └── PatentSearchAgent
└── AnalysisTeamAgent
├── StatisticalAnalysisAgent
├── SentimentAnalysisAgent
└── TrendIdentificationAgent
Each level in the hierarchy represents a more focused specialization.
Benefits for AI Agent Systems
Integrating the Actor Model with supervision provides Theater with:
- Clear Agent Boundaries: A natural model for defining the scope and capabilities of individual agents.
- Enhanced Fault Tolerance: The ability to contain failures and automatically recover parts of the agent system.
- Scalability: Agents can potentially be distributed across cores or machines to handle increased load.
- Resilience: Systems can remain partially or fully operational even when individual agents fail.
- Modular Evolution: Agents can often be developed, deployed, and updated independently, facilitating continuous improvement without system downtime.
Combined with WebAssembly components, the Actor Model allows Theater to manage complex, evolving agent systems within a structure designed for resilience and adaptability.
Traceability & Verification for AI Agents
Traceability and verification form the third pillar of Theater, ensuring transparency, auditability, and reproducibility of AI agent behavior. Theater achieves this by meticulously recording all agent actions, decisions, and state changes in a verifiable structure known as the Event Chain. This provides unprecedented insight into agent operations, crucial for debugging, analysis, and building trust in autonomous systems.
The Event Chain: A Verifiable History of Agent Actions
At the heart of Theater's traceability lies the Event Chain. Think of it as an immutable, comprehensive logbook that records everything significant that happens within an agent system.
- Boundary Monitoring: The system monitors the boundary of each agent (a WebAssembly Component). Every piece of information crossing this boundary – inputs to the agent, outputs returned, messages sent or received – is captured as an event.
- Comprehensive Recording: Events include agent creation/termination, message sends/receives, API calls made by agents, return values, state changes, external inputs/outputs, and errors.
- Cryptographic Linking: Each event recorded in the chain includes a cryptographic hash of the previous event. This creates a tamper-evident sequence; any modification to a past event would invalidate the hashes of all subsequent events, making unauthorized changes detectable.
This chain provides a complete, verifiable history of each agent's actions and the overall system's operation.
Deterministic Replay for Debugging and Verification
Because WebAssembly execution is deterministic and all inputs are captured in the Event Chain, Theater can precisely replay past agent behavior:
- Reproduce Behaviors: If an agent produces unexpected results, the exact sequence of events leading up to it can be replayed in a controlled environment to reliably reproduce the behavior.
- Debug Complex Interactions: Developers can step through the replayed sequence, examining agent states and messages at each point to understand complex decision processes or pinpoint the cause of issues.
- Verify Improvements: After modifying agent code to fix a problem, the original event sequence can be replayed against the new agent version to confirm the improvement and check for regressions.
- Understand Emergent Behaviors: When multiple agents interact in complex ways, replaying those interactions helps understand emergent system behaviors.
This capability is invaluable for understanding and debugging AI agent systems, especially as agents become more sophisticated and their internal logic more complex.
Verifiable State Management
Theater's approach to agent state management is tightly integrated with traceability:
- Explicit State Operations: Agents interact with their persistent state via specific host functions provided by Theater.
- State Changes as Events: Every modification to an agent's state is recorded as an event in the Event Chain, linked to the causal trigger (e.g., processing a specific message or API response).
- State History: The complete evolution of an agent's state is available for inspection, showing how the agent's internal model evolved over time.
This ensures that not only the external actions but also the internal state evolution of each agent is fully captured and verifiable.
Agent Decision Transparency
For AI agents, transparency into decision-making processes is crucial for trust and debugging:
- Input Capture: All inputs that influenced an agent's decisions are recorded
- State Transitions: Changes to the agent's internal state that led to decisions are tracked
- Output Tracing: All actions taken by the agent are linked to the inputs and state that caused them
- Causal Chains: The complete chain of causality from input to action is preserved
This level of transparency transforms "black box" AI agents into auditable systems whose behavior can be fully understood and verified.
Inspection and Analysis Tools
The Event Chain serves as a rich data source for understanding agent behavior. Theater aims to provide tools (or enable the building of tools) for:
- Event Inspection: Browse and examine individual agent actions and their associated data
- Timeline Visualization: View the sequence of interactions between agents over time
- State History: Track how an agent's internal state evolved in response to events
- Causality Analysis: Trace dependencies between events to understand cause-and-effect relationships
- Decision Trees: Visualize the decision paths taken by agents based on different inputs
Benefits for AI Agent Systems
The Traceability & Verification pillar provides:
- Transparency: Making agent behavior fully observable and understandable
- Powerful Debugging: Enabling precise reproduction and diagnosis of unexpected behaviors
- Auditability: Allowing independent verification of agent actions and decision processes
- Enhanced Trust: Providing strong evidence of agent behavior, critical for security and compliance
- Continuous Improvement: Facilitating better agent development through comprehensive feedback
By capturing a verifiable record of all agent actions, Theater provides the tools needed to understand, debug, and ultimately trust autonomous agent systems. This is especially valuable as agents become more capable and are deployed in increasingly critical applications.
From Black Box to Glass Box
Traditional AI systems often operate as "black boxes" where inputs go in, outputs come out, but the internal process remains opaque. Theater transforms AI agents into "glass box" systems where:
- Every input is recorded
- Every state change is tracked
- Every decision is logged
- Every action is auditable
This transparency is essential for building trustworthy AI agent systems that can be deployed with confidence in production environments. Whether for regulatory compliance, user trust, debugging, or system improvement, Theater's traceability capabilities provide the visibility needed to understand and verify agent behavior.
Configuration Reference
Theater uses TOML for configuration, with support for both actor manifests and system configuration.
Actor Manifest
The basic structure of an actor manifest:
# Basic actor information
name = "my-actor"
component_path = "path/to/actor.wasm"
# Interface definitions
[interface]
implements = [
"ntwk:simple-actor/actor",
"ntwk:simple-actor/http-server"
]
requires = ["ntwk:simple-actor/http-client"]
# Handler configurations
[[handlers]]
type = "Http-server"
config = { port = 8080 }
[[handlers]]
type = "Metrics"
config = { path = "/metrics" }
Core Fields
name
(required): Unique identifier for the actorcomponent_path
(required): Path to the WebAssembly componentdescription
(optional): Human-readable descriptionversion
(optional): Semantic version of the actor
Interface Configuration
[interface]
# Interfaces this actor implements
implements = [
"ntwk:simple-actor/actor", # Core actor interface
"ntwk:simple-actor/metrics", # Metrics exposure
"my-org:custom/interface" # Custom interfaces
]
# Interfaces this actor requires
requires = [
"ntwk:simple-actor/http-client"
]
# Optional interface configuration
[interface.config]
timeout_ms = 5000
retry_count = 3
Handler Types
HTTP Server
[[handlers]]
type = "Http-server"
config = {
port = 8080,
host = "127.0.0.1", # Optional, defaults to 0.0.0.0
path_prefix = "/api", # Optional base path
# Optional TLS configuration
tls = {
cert_path = "/path/to/cert.pem",
key_path = "/path/to/key.pem"
}
}
HTTP Client
[[handlers]]
type = "Http-client"
config = {
base_url = "https://api.example.com",
timeout_ms = 5000,
# Optional default headers
headers = {
"User-Agent" = "Theater/1.0",
"Authorization" = "Bearer ${ENV_TOKEN}"
}
}
Metrics Handler
[[handlers]]
type = "Metrics"
config = {
path = "/metrics",
port = 9090, # Optional, uses main HTTP port if not specified
format = "prometheus"
}
Custom Handler
[[handlers]]
type = "Custom"
name = "my-handler"
config = {
# Handler-specific configuration
setting1 = "value1",
setting2 = 42
}
State Configuration
[state]
# Initial state as JSON
initial = """
{
"count": 0,
"created_at": "${NOW}"
}
"""
# Optional state validation
[state.validation]
schema = "path/to/schema.json"
Environment Variables
Configuration values can reference environment variables:
${ENV_NAME}
: Required environment variable${ENV_NAME:-default}
: Environment variable with default${NOW}
: Current timestamp${UUID}
: Generate unique ID
Example:
name = "actor-${ENV_INSTANCE_ID:-001}"
component_path = "${COMPONENT_DIR}/actor.wasm"
[interface.config]
api_key = "${API_KEY}"
System Configuration
Theater system-wide configuration:
# System configuration file: theater.toml
[system]
# Directory for WebAssembly components
component_dir = "/opt/theater/components"
# Logging configuration
[system.logging]
level = "info"
format = "json"
output = "stdout"
# Hash chain storage
[system.storage]
type = "filesystem"
path = "/var/lib/theater/chains"
# Optional distributed configuration
[system.distributed]
discovery = "consul"
consul_url = "http://localhost:8500"
Actor Loading
[system.loading]
# Allow actors to be loaded from these directories
allowed_paths = [
"/opt/theater/components",
"${HOME}/.theater/components"
]
# Component validation
verify_signatures = true
signature_keys = ["path/to/public.key"]
Resource Limits
[system.limits]
# Memory limits
max_memory_mb = 512
max_state_size_mb = 10
# Execution limits
max_execution_time_ms = 1000
max_message_size_kb = 64
# Handler limits
max_http_connections = 1000
max_handlers_per_actor = 5
Security Settings
[system.security]
# WASM execution
enable_bulk_memory = true
enable_threads = false
enable_simd = false
# Network access
allow_outbound = true
allowed_hosts = [
"api.example.com",
"*.internal.org"
]
# File system access
allow_fs_read = true
allow_fs_write = false
allowed_paths = ["/var/lib/theater"]
Development Configuration
For development environments:
[dev]
# Hot reload configuration
watch_paths = ["src", "components"]
reload_on_change = true
# Development-specific handlers
[[dev.handlers]]
type = "Http-server"
config = { port = 3000 }
# Mock external services
[[dev.mocks]]
name = "external-api"
port = 8081
responses = "path/to/mock/responses.json"
Best Practices
-
Configuration Organization
- Keep configurations in dedicated directory
- Use environment variables for secrets
- Version control templates, not actual configs
- Document all custom values
-
Security
- Never commit sensitive values
- Use environment variables for credentials
- Restrict file system access
- Limit network access
-
Development
- Use separate dev configurations
- Enable detailed logging
- Configure mock services
- Set reasonable resource limits
-
Deployment
- Use environment-specific configs
- Validate all configurations
- Monitor resource limits
- Document all settings
-
Maintenance
- Regular config reviews
- Update security settings
- Clean up unused configs
- Track config changes
Theater CLI
The Theater CLI is a command-line tool that provides a convenient interface for working with the Theater WebAssembly actor system. It simplifies actor development, deployment, and management.
Installation
The Theater CLI is included when you build the Theater project:
# Build the project
cargo build --release
# Use the CLI directly
./target/release/theater --help
# Or add it to your PATH for easier access
Basic Usage
# Get help
theater --help
# Run commands with verbose output
theater -v <command>
# Get output in JSON format (for scripting)
theater --json <command>
Command Overview
Command | Description |
---|---|
build | Build a Theater actor to WebAssembly |
create | Create a new Theater actor project |
events | Get actor events |
inspect | Show detailed information about an actor |
list | List all running actors |
logs | View actor logs |
message | Send a message to an actor |
restart | Restart a running actor |
server | Start a Theater server |
shell | Start an interactive shell |
start | Start an actor from a manifest |
state | Get actor state |
stop | Stop a running actor |
tree | Show actor hierarchy as a tree |
validate | Validate an actor manifest |
watch | Watch a directory and redeploy on changes |
Detailed Command Usage
Creating a New Actor Project
# Create a basic actor project
theater create my-actor
# Create an HTTP actor project
theater create my-http-actor --template http
# Create a project in a specific directory
theater create my-actor --output-dir ~/projects
Building a Theater Actor
# Build the actor in the current directory
theater build
# Build a specific project
theater build /path/to/project
# Build in debug mode
theater build --release false
# Clean and rebuild
theater build --clean
Managing a Theater Server
# Start a server with default settings
theater server
# Start a server with a custom port
theater server --port 9001
# Start a server with a custom data directory
theater server --data-dir /path/to/data
Running Actors
# Start an actor from a manifest
theater start path/to/manifest.toml
# Start an actor and output only its ID (useful for piping)
theater start path/to/manifest.toml --id-only
# Start an actor and monitor its events
theater start path/to/manifest.toml --monitor
# List all running actors
theater list
# View actor logs
theater logs <actor-id>
# Get actor state
theater state <actor-id>
# Get actor events
theater events <actor-id>
# Stop an actor
theater stop <actor-id>
# Restart an actor
theater restart <actor-id>
# Subscribe to actor events
theater subscribe <actor-id>
# Start an actor and subscribe to its events (piping commands)
theater start path/to/manifest.toml --id-only | theater subscribe -
Development Workflow
# Create a new actor project
theater create my-actor
# Build the actor
cd my-actor
theater build
# Start the actor and monitor its events
theater start manifest.toml --monitor
# Or, start without monitoring
theater start manifest.toml
# Watch the directory and redeploy on changes
theater watch . --manifest manifest.toml
Sending Messages to Actors
# Send a JSON message to an actor
theater message <actor-id> --data '{"action": "doSomething", "value": 42}'
# Send a message from a file
theater message <actor-id> --file message.json
Output Formats
The Theater CLI supports human-readable output (default) and JSON output for scripting:
# Human-readable output
theater list
# JSON output
theater --json list
Environment Variables
The Theater CLI respects the following environment variables:
THEATER_SERVER_ADDRESS
: Default server address (host:port)THEATER_DATA_DIR
: Default data directory location
Common Workflows
Develop, Build, and Run Loop
-
Create a new actor project
theater create my-actor cd my-actor
-
Build the actor
theater build
-
Start a Theater server (in another terminal)
theater server
-
Start the actor
theater start manifest.toml
-
Watch for changes and automatically redeploy
theater watch . --manifest manifest.toml
Monitoring and Debugging
To monitor and debug actors:
-
List all running actors
theater list
-
View actor logs
theater logs <actor-id>
-
Inspect actor state
theater state <actor-id>
-
View actor events
theater events <actor-id>
-
Monitor actor events in real-time
# When starting a new actor theater start manifest.toml --monitor # Or use the subscribe command theater subscribe <actor-id> # Or pipe commands together for a streamlined workflow theater start manifest.toml --id-only | theater subscribe -
-
Restart an actor if issues occur
theater restart <actor-id>
Advanced Usage
HTTP Actor Setup
For HTTP actors:
-
Create an HTTP actor project
theater create my-http-actor --template http
-
Build and start
cd my-http-actor theater build theater start manifest.toml
-
The HTTP server will be available at the port specified in the manifest
Supervisor Pattern
For parent-child actor relationships:
- Create parent and child actors
- Configure the parent with supervisor capabilities
- Start the parent actor
- The parent can then spawn and manage child actors
New Advanced Commands
Inspecting Actors
# Inspect an actor in detail
theater inspect <actor-id>
# Show detailed view with full state and all events
theater inspect <actor-id> --detailed
Visualizing Actor Hierarchies
# View actor hierarchy as a tree
theater tree
# Limit tree depth
theater tree --depth 2
# Show tree starting from a specific actor
theater tree --root <actor-id>
Validating Manifests
# Validate an actor manifest
theater validate path/to/manifest.toml
# Validate with interface compatibility check
theater validate path/to/manifest.toml --check-interfaces
Interactive Shell
Theater provides an interactive shell for working with actors:
# Start the interactive shell
theater shell
# Connect to a custom server
theater shell --address 127.0.0.1:9001
In the shell, you can run commands like:
list
- List all running actorsinspect <id>
- Show detailed information about an actorstate <id>
- Show the current state of an actorevents <id>
- Show events for an actorstart <path>
- Start an actor from a manifeststop <id>
- Stop a running actorrestart <id>
- Restart a running actormessage <id> <msg>
- Send a message to an actorclear
- Clear the screenhelp
- Show helpexit
- Exit the shell
Tips and Tricks
- Use the
--verbose
flag for detailed output during commands - Use the
--json
flag to get structured output for scripting - For faster development, use the
watch
command for automatic redeployment - Use the
start --monitor
flag to start an actor and monitor its events in real-time - For more advanced event monitoring, use the
subscribe
command with filtering options - Combine commands with pipes:
theater start manifest.toml --id-only | theater subscribe -
- The
subscribe
command supports various filtering options like--event-type
,--detailed
, and--limit
- Check
theater --help
andtheater <command> --help
for specific command options
Troubleshooting
Building AI Agent Systems
Running AI-Generated Code Safely
AI code generation has become increasingly powerful and prevalent. Models like GPT-4, Claude, and specialized coding assistants can now write complex functions, entire modules, and even complete applications with minimal human guidance. This capability offers tremendous productivity benefits, but also introduces new challenges in terms of code quality, reliability, and security.
Theater was designed with these challenges in mind, providing a robust framework for running AI-generated code safely and effectively.
The AI Code Generation Landscape
Before diving into how Theater helps, let's understand the current landscape of AI code generation:
Strengths of AI-Generated Code
- Speed and Volume: AI can generate large amounts of code quickly
- Breadth of Knowledge: Modern LLMs have been trained on vast repositories of code across languages and frameworks
- Pattern Replication: AI excels at implementing standard patterns and boilerplate code
- Adaptation: AI can often adapt code to new contexts or requirements with minimal guidance
Challenges with AI-Generated Code
- Correctness Validation: Verifying that large volumes of AI-generated code work correctly
- Subtle Bugs: AI can introduce subtle logical errors that pass syntax checks but cause runtime issues
- Security Vulnerabilities: AI might inadvertently replicate insecure patterns from its training data
- Debugging Complexity: Understanding and fixing issues in code you didn't write
- Integration Problems: Ensuring AI-generated components work properly with the rest of your system
How Theater Addresses These Challenges
Theater provides a comprehensive solution for running AI-generated code safely:
1. Containment through WebAssembly Sandboxing
AI-generated code in Theater runs within WebAssembly sandboxes, which:
- Prevent direct access to the host system or other components
- Limit resource consumption through configurable limits
- Create clear boundaries around what the code can and cannot do
- Enable the safe execution of code that hasn't been thoroughly reviewed
#![allow(unused)] fn main() { // Example of compiling AI-generated code to WebAssembly fn compile_ai_code(source: &str) -> Result<Vec<u8>, CompileError> { // Compile the AI-generated code to WebAssembly let wasm_binary = your_compiler.compile(source)?; // Return the WebAssembly binary Ok(wasm_binary) } // Load the WebAssembly component into Theater let actor_id = theater.load_component(&wasm_binary, &manifest)?; }
2. Fault Isolation through Actor Supervision
Theater's supervision system ensures that failures in AI-generated code don't cascade through your entire application:
- Parent actors can monitor child actors (which might be AI-generated)
- If a child actor fails, the parent can restart it or take other recovery actions
- Failures are contained to the specific actor that encountered the issue
- The system as a whole remains stable even if individual components fail
#![allow(unused)] fn main() { // Example of a supervision strategy for AI-generated actors use theater::supervisor::*; fn handle_child_failure(child_id: &ActorId, error: &Error) -> SupervisorAction { match error { // For temporary errors, restart the actor Error::Temporary(_) => SupervisorAction::Restart, // For more serious errors, stop the actor and notify the admin Error::Critical(_) => { notify_admin(child_id, error); SupervisorAction::Stop } } } }
3. Traceability for Debugging and Improvement
One of the biggest challenges with AI-generated code is understanding what went wrong when issues occur. Theater's traceability features address this directly:
- Every state change is recorded in a verifiable chain
- All inputs and outputs are captured for later analysis
- Developers can trace the exact sequence of events that led to a failure
- This information can be used to improve the AI code generation process
#![allow(unused)] fn main() { // Example of reviewing state history for an AI-generated actor let history = theater.get_state_history(actor_id)?; // Analyze the history to find the cause of the issue for state in history { println!("State at {}: {:?}", state.timestamp, state.data); // Look for the state change that caused the problem if let Some(problem) = identify_problem(&state) { println!("Found potential issue: {}", problem); // Use this information to improve the prompt for the AI let improved_prompt = generate_improved_prompt(problem); println!("Suggested prompt improvement: {}", improved_prompt); } } }
Practical Patterns for AI-Generated Actors
When working with AI-generated code in Theater, consider these patterns:
1. Incremental Responsibility
Start by giving AI-generated actors small, well-defined responsibilities, then gradually increase their scope as you gain confidence:
- Begin with simple data transformation actors
- Progress to actors that maintain internal state
- Eventually allow AI-generated actors to spawn and supervise other actors
2. Clear Interface Boundaries
Define clear interfaces for your AI-generated actors:
# Example manifest for an AI-generated actor
name = "ai-generated-processor"
component_path = "ai_processor.wasm"
[interface]
implements = "ntwk:data-processing/processor"
requires = []
[[handlers]]
type = "message-server"
config = {}
By strictly defining the interfaces, you constrain what the AI-generated code needs to do and limit the potential impact of issues.
3. Supervision Hierarchies
Design your supervision hierarchies to properly manage AI-generated components:
- Human-written supervisor actors at the top levels
- AI-generated actors in the middle or leaf positions
- Critical systems supervised by human-written code
- Non-critical systems can be supervised by other AI-generated actors
4. Continuous Verification
Use Theater's traceability features to continuously verify the behavior of AI-generated actors:
- Set up automated tests that verify state transitions
- Monitor for unexpected patterns in actor behavior
- Use the collected data to improve future iterations of the AI-generated code
Case Study: AI-Generated Microservices
A compelling use case for Theater is running a network of AI-generated microservices. In this scenario:
- Each microservice is implemented as a Theater actor
- The services communicate through well-defined message interfaces
- A supervision hierarchy ensures system stability
- Complete traceability provides visibility into the entire system
This approach allows organizations to rapidly develop and deploy new services, leveraging AI for code generation while maintaining system reliability and security.
Future Directions
The integration of AI code generation with Theater is still evolving. Some exciting future directions include:
- Feedback Loops: Automatically using state history and failure data to improve AI prompts
- Self-Healing Systems: AI-powered supervisors that learn from past failures to improve recovery strategies
- Hybrid Development: Tools that seamlessly blend human and AI-written components within the Theater framework
By providing a structured, safe environment for running AI-generated code, Theater enables developers to confidently embrace the productivity benefits of AI while mitigating the associated risks.
Building Actors in Theater
This guide walks you through creating actors in Theater, from basic concepts to advanced patterns, with practical examples.
Quick Start
Create a new actor project:
cargo new my-actor
cd my-actor
Add dependencies to Cargo.toml:
[package]
name = "my-actor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
Project Structure
my-actor/
├── Cargo.toml # Project configuration
├── actor.toml # Actor manifest
├── src/
│ ├── lib.rs # Actor implementation
│ └── state.rs # State management
└── wit/ # Interface definitions
└── actor.wit # Actor interface
Basic Actor Implementation
Here's a complete example of a simple counter actor:
#![allow(unused)] fn main() { // src/lib.rs use bindings::exports::ntwk::theater::actor::Guest as ActorGuest; use bindings::ntwk::theater::types::{Event, Json}; use bindings::ntwk::theater::runtime::log; use serde::{Deserialize, Serialize}; // Define actor state #[derive(Serialize, Deserialize)] struct State { count: i32, last_updated: String, } // Define message types #[derive(Deserialize)] #[serde(tag = "type")] enum Message { Increment { amount: i32 }, Decrement { amount: i32 }, Reset, } struct Component; impl ActorGuest for Component { fn init() -> Vec<u8> { log("Initializing counter actor"); let initial_state = State { count: 0, last_updated: chrono::Utc::now().to_string(), }; serde_json::to_vec(&initial_state).unwrap() } fn handle(evt: Event, state: Vec<u8>) -> Vec<u8> { log(&format!("Handling event: {:?}", evt)); let mut current_state: State = serde_json::from_slice(&state).unwrap(); if let Ok(message) = serde_json::from_slice(&evt.data) { match message { Message::Increment { amount } => { current_state.count += amount; } Message::Decrement { amount } => { current_state.count -= amount; } Message::Reset => { current_state.count = 0; } } current_state.last_updated = chrono::Utc::now().to_string(); } serde_json::to_vec(¤t_state).unwrap() } } bindings::export!(Component with_types_in bindings); }
Actor Manifest
Configure your actor in actor.toml:
name = "counter-actor"
component_path = "target/wasm32-wasi/release/counter_actor.wasm"
[interface]
implements = "ntwk:theater/actor"
requires = []
[[handlers]]
type = "http-server"
config = { port = 8080 }
[logging]
level = "debug"
output = "stdout"
Adding HTTP Capabilities
Extend the actor to handle HTTP requests:
#![allow(unused)] fn main() { use bindings::exports::ntwk::theater::http_server::Guest as HttpGuest; use bindings::ntwk::theater::http_server::{HttpRequest, HttpResponse}; impl HttpGuest for Component { fn handle_request(req: HttpRequest, state: Json) -> (HttpResponse, Json) { match (req.method.as_str(), req.path.as_str()) { // Get current count ("GET", "/count") => { let current_state: State = serde_json::from_slice(&state).unwrap(); (HttpResponse { status: 200, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()) ], body: Some(serde_json::json!({ "count": current_state.count, "last_updated": current_state.last_updated }).to_string().into_bytes()), }, state) }, // Increment count ("POST", "/increment") => { if let Some(body) = req.body { if let Ok(increment) = serde_json::from_slice::<serde_json::Value>(&body) { let amount = increment["amount"].as_i64().unwrap_or(1) as i32; let evt = Event { event_type: "increment".to_string(), parent: None, data: serde_json::json!({ "type": "Increment", "amount": amount }).to_string().into_bytes(), }; let new_state = Component::handle(evt, state); return (HttpResponse { status: 200, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()) ], body: Some(b"{"status":"ok"}".to_vec()), }, new_state); } } (HttpResponse { status: 400, headers: vec![], body: Some(b"{"error":"invalid request"}".to_vec()), }, state) }, _ => (HttpResponse { status: 404, headers: vec![], body: None, }, state) } } } }
Adding WebSocket Support
Enable real-time updates with WebSocket support:
#![allow(unused)] fn main() { use bindings::exports::ntwk::theater::websocket_server::Guest as WebSocketGuest; use bindings::ntwk::theater::websocket_server::{ WebSocketMessage, WebSocketResponse, MessageType }; impl WebSocketGuest for Component { fn handle_message(msg: WebSocketMessage, state: Json) -> (Json, WebSocketResponse) { match msg.ty { MessageType::Text => { if let Some(text) = msg.text { // Parse command if let Ok(command) = serde_json::from_str::<serde_json::Value>(&text) { match command["action"].as_str() { Some("subscribe") => { // Send current state let current_state: State = serde_json::from_slice(&state).unwrap(); return (state, WebSocketResponse { messages: vec![WebSocketMessage { ty: MessageType::Text, text: Some(serde_json::json!({ "type": "update", "count": current_state.count }).to_string()), data: None, }] }); }, _ => {} } } } }, _ => {} } (state, WebSocketResponse { messages: vec![] }) } } }
Using Host Functions
Theater provides several host functions for common operations:
#![allow(unused)] fn main() { use bindings::ntwk::theater::runtime::{log, spawn}; use bindings::ntwk::theater::filesystem::read_file; // Logging log("Actor processing message..."); // Spawn another actor spawn("other-actor.toml"); // Read a file let content = read_file("config.json"); }
State Management Best Practices
- Use Strong Typing
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] struct State { data: HashMap<String, Value>, metadata: Metadata, updated_at: DateTime<Utc>, } #[derive(Serialize, Deserialize)] struct Metadata { version: u32, owner: String, } }
- Handle Errors Gracefully
#![allow(unused)] fn main() { fn handle(evt: Event, state: Json) -> Json { let current_state: State = match serde_json::from_slice(&state) { Ok(state) => state, Err(e) => { log(&format!("Error parsing state: {}", e)); return state; // Return unchanged state on error } }; // Process event... } }
- Include Timestamps
#![allow(unused)] fn main() { fn update_state(mut state: State) -> State { state.updated_at = chrono::Utc::now(); state } }
Testing
Create tests for your actor:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_increment() { let state = State { count: 0, last_updated: chrono::Utc::now().to_string(), }; let event = Event { event_type: "increment".to_string(), parent: None, data: serde_json::json!({ "type": "Increment", "amount": 5 }).to_string().into_bytes(), }; let state_json = serde_json::to_vec(&state).unwrap(); let new_state_json = Component::handle(event, state_json); let new_state: State = serde_json::from_slice(&new_state_json).unwrap(); assert_eq!(new_state.count, 5); } } }
Advanced Patterns
1. State History
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] struct State { current: StateData, history: VecDeque<StateChange>, } #[derive(Serialize, Deserialize)] struct StateChange { timestamp: DateTime<Utc>, change_type: String, previous_value: Value, } }
2. Event Correlation
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] struct Event { id: String, correlation_id: Option<String>, causation_id: Option<String>, data: Value, } }
3. Validation Chain
#![allow(unused)] fn main() { fn validate_state(state: &State) -> Result<(), String> { validate_constraints(state)?; validate_relationships(state)?; validate_business_rules(state)?; Ok(()) } }
Development Tips
- Use the runtime log function liberally
- Test with different message types
- Verify state transitions
- Handle all error cases
- Monitor the hash chain
- Test all handler interfaces
Common Pitfalls
-
Not Handling JSON Errors
- Always handle deserialization errors
- Validate JSON structure
- Handle missing fields
-
State Inconsistency
- Validate state after changes
- Keep state updates atomic
- Handle partial updates
-
Missing Error Logging
- Log all errors
- Include context
- Track error patterns
-
Resource Management
- Clean up resources
- Handle timeouts
- Monitor memory usage
Building Host Functions Guide
This guide explores the principles, challenges, and best practices for implementing host functions in Theater, with particular focus on handling asynchronous operations and maintaining the actor system's integrity.
Core Principles
1. Consistent Parameter Patterns
- WIT interfaces should use tuple-based parameter patterns
- Client functions should always receive state as their first parameter
- Parameters should be bundled in tuples for consistency
2. State Chain Integrity
- Every state transition must be properly recorded in the hash chain
- State updates must be atomic and consistent
- The chain must remain verifiable at all times
2. Non-Blocking Operation
- Host functions should avoid blocking the actor system
- Long-running operations should be structured to allow progress
- State transitions should be quick and deterministic
3. Sequential Guarantee Management
- WebAssembly component calls are inherently sequential
- Host functions must be designed with this limitation in mind
- Complex async operations need careful structuring
Common Challenges
Sequential Call Limitation
The WebAssembly component model requires that calls be sequential and return before making progress. This creates challenges for operations that are inherently concurrent or long-running, such as:
- Websocket connections
- Long-polling HTTP requests
- File watchers
- Database connections
Solutions and Patterns
1. Event Queue Pattern
Instead of blocking on handlers, implement an event queue system:
#![allow(unused)] fn main() { struct WebSocketHost { event_queue: Arc<Mutex<VecDeque<WebSocketEvent>>>, connections: Arc<Mutex<HashMap<ConnectionId, WebSocket>>>, } enum WebSocketEvent { NewConnection(ConnectionId, WebSocket), Message(ConnectionId, Message), Disconnection(ConnectionId), } impl WebSocketHost { fn process_events(&mut self) { while let Some(event) = self.event_queue.lock().unwrap().pop_front() { match event { WebSocketEvent::NewConnection(id, ws) => { // Handle new connection without blocking self.connections.lock().unwrap().insert(id, ws); // Notify actor of new connection self.notify_actor_connection(id); } // Handle other events... } } } } }
2. State Machine Approach
Model long-running operations as state machines:
#![allow(unused)] fn main() { enum ConnectionState { Connecting, Connected(WebSocket), Closing, Closed, } struct Connection { state: ConnectionState, events: VecDeque<WebSocketEvent>, last_processed: Instant, } }
3. Async Operation Splitting
Break long-running operations into discrete steps:
- Operation initiation
- Progress checking
- Result collection
Best Practices
-
Event Buffering
- Buffer events when they can't be processed immediately
- Implement reasonable buffer limits
- Handle buffer overflow gracefully
-
Resource Management
- Track resource usage carefully
- Implement proper cleanup mechanisms
- Handle resource exhaustion gracefully
-
Error Handling
- Propagate errors appropriately
- Maintain system stability during errors
- Log errors with context for debugging
-
State Consistency
- Ensure state transitions are atomic
- Validate state after transitions
- Handle partial failures gracefully
Interface Design Guidelines
1. WIT Interface Design
- Define client-side functions with consistent state parameter patterns:
handle-function: func(state: option<json>, params: tuple<param1-type, param2-type>) -> result<tuple<option<json>, return-type>, string>;
- The first parameter is always the actor's state
- The second parameter is always a tuple containing function parameters
- The result includes both the new state and function result
2. Host Implementation
- When implementing host-side code that calls client functions, use natural Rust syntax:
#![allow(unused)] fn main() { actor_handle .call_function::<(ParamType1, ParamType2), ReturnType>( "interface.function-name", (param1, param2), ) .await?; }
- The type parameters to
call_function
should match the WIT interface - The adapter layer handles wrapping parameters to match the tuple-based interface
3. Function Registration
- Register functions with types matching the WIT interface:
#![allow(unused)] fn main() { actor_instance .register_function_no_result::<(ParamType1, ParamType2)>( "interface", "function-name", ) }
4. Example: Channel Functions
-
For channel operations, follow the same parameter pattern:
WIT Interface:
// Correct pattern with tuple-based parameters handle-channel-message: func(state: option<json>, params: tuple<channel-id, json>) -> result<tuple<option<json>>, string>; handle-channel-close: func(state: option<json>, params: tuple<channel-id>) -> result<tuple<option<json>>, string>;
Host Implementation:
#![allow(unused)] fn main() { // Standard Rust syntax for calling the functions actor_handle .call_function::<(String, Vec<u8>), ()>( "ntwk:theater/message-server-client.handle-channel-message", (channel_id.to_string(), data), ) .await?; }
Function Registration:
#![allow(unused)] fn main() { // Register with types matching the WIT interface actor_instance .register_function_no_result::<(String, Vec<u8>)>( "ntwk:theater/message-server-client", "handle-channel-message", ) }
Implementation Guidelines
1. Planning Phase
- Map out all possible states and transitions
- Identify potential blocking operations
- Plan error handling strategy
- Consider resource limitations
2. Implementation Phase
- Start with a clear state model
- Implement event buffering early
- Add comprehensive logging
- Build in failure handling
3. Testing Phase
- Test concurrent operations
- Verify state consistency
- Check resource cleanup
- Test error conditions
WebSocket Host Example
Here's an improved approach to WebSocket hosting:
#![allow(unused)] fn main() { struct WebSocketHost { connections: Arc<Mutex<HashMap<ConnectionId, Connection>>>, event_queue: Arc<Mutex<VecDeque<WebSocketEvent>>>, config: WebSocketConfig, } impl WebSocketHost { fn process_events(&mut self) -> Result<(), HostError> { // Process a batch of events let mut events = self.event_queue.lock().unwrap(); let batch: Vec<_> = events.drain(..min(events.len(), MAX_BATCH_SIZE)).collect(); for event in batch { match event { WebSocketEvent::NewConnection(id, ws) => { self.handle_new_connection(id, ws)?; } WebSocketEvent::Message(id, msg) => { self.handle_message(id, msg)?; } WebSocketEvent::Disconnection(id) => { self.handle_disconnection(id)?; } } } Ok(()) } fn handle_new_connection(&mut self, id: ConnectionId, ws: WebSocket) -> Result<(), HostError> { // Add to connections without blocking self.connections.lock().unwrap().insert(id, Connection::new(ws)); // Notify actor through chain self.notify_actor_connection(id) } } }
Understanding Parameter Wrapping
The Theater runtime handles parameter conversion between Rust function calls and WebAssembly interfaces. Here's how it works:
1. Parameter Flow
-
Host Call Layer: When calling
actor_handle.call_function<P, R>(...)
, the parameters are serialized to JSON bytes:#![allow(unused)] fn main() { let params = serde_json::to_vec(¶ms)? }
-
Executor Layer: The
execute_call
function passes state and parameters to the actor instance:#![allow(unused)] fn main() { let (new_state, results) = self.actor_instance.call_function(&name, state, params).await }
-
Adapter Layer: The
TypedFunction
implementation deserializes parameters and calls the appropriately typed function:#![allow(unused)] fn main() { let params_deserialized: P = serde_json::from_slice(¶ms)? match self.call_func(store, state, params_deserialized).await ... }
-
WebAssembly Layer: The parameters are passed to the WebAssembly function according to the WIT interface, with state as the first parameter and parameters as a tuple.
2. Return Flow
-
WebAssembly Layer: The function returns a result containing the new state and return value.
-
Adapter Layer: The result is serialized back to JSON bytes:
#![allow(unused)] fn main() { let result_serialized = serde_json::to_vec(&result)? }
-
Executor Layer: The new state is stored in the actor store:
#![allow(unused)] fn main() { self.actor_instance.store.data_mut().set_state(new_state); }
-
Host Call Layer: The result is deserialized back to the expected return type:
#![allow(unused)] fn main() { let res = serde_json::from_slice::<R>(&result)?; }
3. Type Mapping
The type parameters used in call_function<P, R>
and register_function*
functions should match the WebAssembly interface definition, but the adapter layer handles the specifics of matching the tuple structure. This lets you use natural Rust parameter patterns while maintaining a consistent WIT interface.
Troubleshooting Common Issues
1. Blocking Operations
Problem: Operation blocks progress Solution: Convert to event-based handling
2. Resource Leaks
Problem: Resources not properly cleaned up Solution: Implement proper cleanup in all exit paths
3. State Inconsistency
Problem: State becomes invalid during concurrent operations Solution: Use atomic operations and validate state transitions
4. Parameter Pattern Mismatch
Problem: WIT interface defines tuple parameters but implementation doesn't match Solution: Ensure WIT interface uses consistent tuple pattern for parameters:
// CORRECT
handle-function: func(state: option<json>, params: tuple<type1, type2>) -> ...;
// INCORRECT
handle-function: func(state: option<json>, param1: type1, param2: type2) -> ...;
And ensure the host implementation uses matching types in function registration.
Conclusion
Building host functions requires careful consideration of:
- Consistent parameter patterns in WIT interfaces
- Sequential execution constraints
- State consistency requirements
- Resource management
- Error handling
Following these patterns and guidelines helps create robust, maintainable host functions that work well within Theater's actor system. In particular, consistently using tuple-based parameter patterns in the WIT interface while leveraging the adapter layer to maintain natural Rust code creates a clean separation between interface definition and implementation.
Making Changes to Theater
This document outlines the process for making changes to the Theater project. We use a structured approach to document our changes, making it easier for team members to understand what's happening and why.
Change Process
-
Create a Proposal
- All significant changes start with a proposal
- Create a new markdown file in
/changes/proposals/
- Use the format:
YYYY-MM-DD-brief-description.md
- Follow the proposal template structure (see below)
-
Update In-Progress List
- Add your proposal to
/changes/in-progress.md
- Include the proposal file name and a brief description
- Add your proposal to
-
Work on the Change
- Document your progress in the "Working Notes" section
- Update as you encounter challenges or make decisions
- Keep notes about what worked and what didn't
-
Complete the Change
- Fill out the "Final Notes" section
- Document the final implementation details
- Note any future considerations or follow-up work
- Update
in-progress.md
to mark as complete
Proposal Template
# [Brief Description of Change]
## Description
- What is being changed
- Why this change is necessary
- Expected benefits and potential risks
- Any alternatives considered
## Working Notes
- Ongoing notes about the implementation
- Challenges encountered
- Decisions made and their reasoning
- References to relevant commits or discussions
## Final Notes
- Final implementation details
- What was actually changed
- Any deviations from the original plan
- Lessons learned
- Future considerations
Example
See /changes/proposals/2025-01-29-random-id-system.md
for an example of a completed change proposal.
Tips for Good Change Documentation
- Be explicit about your reasoning
- Document both successful and unsuccessful approaches
- Include relevant code examples or diagrams
- Reference related issues or discussions
- Update regularly during the change process
Benefits
- Makes project evolution more transparent
- Helps new team members understand the codebase
- Creates a knowledge base for future reference
- Facilitates code review and discussion
- Provides context for future changes
System Internals
This section provides a deep dive into the internal architecture and implementation details of Theater. It's designed for contributors and advanced users who want to understand how Theater works under the hood.
Who Should Read This Section
- Contributors working on the Theater codebase
- Advanced users looking to extend or customize Theater
- Curious learners interested in system design principles
What's Covered
This section explores the technical implementation of Theater's core concepts:
- System Architecture: The overall design and component relationships
- Data Flow: How information moves through the system
- Implementation Details: Technical specifics of key subsystems
- ID System: How actors and resources are identified
- Interface System: How components communicate
- State Management: Implementation of the state storage mechanisms
- Store System: Technical details of the content-addressable storage
While the Core Concepts section explains what Theater is and its fundamental principles, this section explains how those principles are implemented in practice.
Navigating This Section
The content is organized from high-level architecture to specific implementation details. If you're new to Theater's internals, we recommend starting with the System Architecture page before diving into specific subsystems.
System Architecture
Theater's architecture is designed around the principles of isolation, determinism, and traceability. This page provides a high-level overview of how the system components interact to deliver these guarantees.
System Components
Theater Runtime
The Theater Runtime is the core orchestration layer that:
- Manages actor lifecycle (creation, execution, termination)
- Implements the supervision system
- Coordinates message delivery between actors
- Maintains the event chain
- Provides access to the store system
Actor Executor
The Actor Executor is responsible for:
- Loading WebAssembly components
- Instantiating component instances
- Providing host functions to components
- Managing component memory and resources
- Executing component functions in response to messages
Event Chain System
The Event Chain tracks:
- All inputs to actors
- All outputs from actors
- State changes
- Error conditions
- Supervision actions
Every action in the system is recorded in a verifiable chain of events that enables:
- Deterministic replay
- Auditing
- Debugging
- System verification
Store System
The content-addressable Store provides:
- Persistent storage for actor state
- Version control for state changes
- Efficient storage through content-addressing
- Verification of stored content
Handler System
Handlers extend actor functionality by providing:
- Access to system services
- Integration with external systems
- Standard capabilities (HTTP, filesystem, etc.)
- Custom functionality through a plugin architecture
Data Flow
-
Input Processing:
- External requests enter through the Theater Server
- Requests are converted to messages
- Messages are recorded in the Event Chain
- Messages are delivered to target actors
-
Actor Execution:
- Actor Executor loads actor component
- Message handlers are invoked
- Actor may access state via the Store
- Actor may use handlers to access services
-
Output Handling:
- Actor responses are recorded in the Event Chain
- Responses are delivered to requesters
- State changes are persisted to the Store
Design Principles
Theater's architecture is built on several key design principles:
-
Isolation through WebAssembly:
- Actors run in sandboxed environments
- Component model provides capability-based security
-
Explicit State Management:
- All state is explicitly managed through the Store
- No hidden or shared state between actors
-
Explicit Communication:
- All communication happens through messages
- No direct actor-to-actor function calls
-
Comprehensive Tracing:
- All system actions are recorded
- Chain provides cryptographic verification
-
Hierarchical Supervision:
- Actors are arranged in supervision trees
- Parent actors manage child lifecycle
These principles work together to create a system that is secure, deterministic, and verifiable, making it ideal for applications where these properties are critical.
Component Relationships
This page details how the various components of Theater interact with each other, providing a deeper understanding of the system's internal architecture.
Core Components
Theater Runtime
The Theater Runtime is the central orchestration component that manages the entire system:
- Relationship with Actor Executor: The Runtime uses the Actor Executor to instantiate and run actors
- Relationship with Store: The Runtime coordinates with the Store for state persistence
- Relationship with Event Chain: The Runtime records all system events in the Chain
Actor Executor
The Actor Executor handles WebAssembly component instantiation and execution:
- Relationship with WASM Components: Loads and instantiates WebAssembly components
- Relationship with Host Functions: Provides host functions to running components
- Relationship with Runtime: Reports execution results back to the Runtime
Store System
The Store provides content-addressable storage for actor state:
- Relationship with Actors: Provides state storage and retrieval for actors
- Relationship with Runtime: Coordinates with the Runtime for state management
- Relationship with Event Chain: State changes are recorded in the Event Chain
Event Chain
The Event Chain records all system events:
- Relationship with Runtime: Receives events from the Runtime
- Relationship with Store: Records state changes from the Store
- Relationship with CLI Tools: Provides data for inspection and debugging
Secondary Components
CLI
The CLI provides user interaction with the Theater system:
- Relationship with Runtime: Sends commands to the Runtime
- Relationship with Event Chain: Retrieves and displays events
- Relationship with Store: Accesses stored content
Handlers
Handlers extend actor functionality:
- Relationship with Actor Executor: Registered with the Executor
- Relationship with Runtime: Managed by the Runtime
- Relationship with Event Chain: Handler invocations are recorded
Component Interaction Patterns
Creation Flow
The sequence of component interactions during actor creation:
- CLI or parent actor requests actor creation
- Runtime processes request
- Store retrieves component bytes
- Actor Executor instantiates component
- Runtime initializes actor state
- Event Chain records creation
Message Processing Flow
The sequence of component interactions during message processing:
- Message arrives at Runtime
- Runtime records message in Event Chain
- Runtime delivers message to target actor
- Actor Executor invokes appropriate handler
- Actor may access state via Store
- Actor response is recorded in Event Chain
- Response is delivered to sender
Failure Handling Flow
The sequence of component interactions during failure handling:
- Actor Executor detects failure
- Runtime records failure in Event Chain
- Runtime notifies supervisor
- Supervisor decides on action
- Runtime implements supervisory action
- Event Chain records recovery action
Understanding these component relationships and interaction patterns provides insight into how Theater operates internally and how its various parts work together to create a cohesive system.
Data Flow
This page explains how data moves through the Theater system, from external inputs to actor processing and eventual outputs.
External Input Processing
HTTP Requests
For HTTP-based interactions:
- HTTP request arrives at Theater Server
- Server parses request and identifies target actor
- Request is converted to a message
- Message is recorded in Event Chain
- Message is queued for delivery to actor
CLI Commands
For CLI-initiated actions:
- User issues command via CLI
- CLI connects to Theater Server
- Command is converted to management message
- Message is recorded in Event Chain
- Server processes management command
Message Delivery
For actor-to-actor messaging:
- Sender actor invokes messaging API
- Message is recorded in Event Chain
- Message is queued for delivery
- Target actor receives message
Actor Processing
State Retrieval
When actors access state:
- Actor invokes Store API
- Runtime validates access request
- State request is recorded in Event Chain
- Store retrieves state data
- Data is returned to actor
Computation
During actor computation:
- Actor processes message data
- Actor may access state via Store
- Actor may use handlers for external services
- All handler invocations are recorded in Event Chain
State Updates
When actors modify state:
- Actor creates new state data
- Actor invokes Store API to save state
- Update request is recorded in Event Chain
- Store validates and persists new state
- Store returns new state reference to actor
External Output Processing
HTTP Responses
For HTTP response generation:
- Actor produces response data
- Response is recorded in Event Chain
- Response is converted to HTTP format
- HTTP response is sent to client
Event Streaming
For event subscription:
- Client subscribes to events via Theater Server
- New events are recorded in Event Chain
- Server filters events based on subscription
- Matching events are streamed to client
Special Data Flows
Supervision
During supervision operations:
- Runtime detects actor failure
- Failure is recorded in Event Chain
- Supervisor actor is notified
- Supervisor decides on action
- Supervisory action is recorded in Event Chain
- Runtime implements supervisory action
Replay
During event replay:
- Replay request identifies starting point
- Events are retrieved from Event Chain
- Actor state is initialized
- Events are applied sequentially
- Actor reaches target state
Understanding these data flows helps visualize how information moves through the Theater system and how different components collaborate to process data securely and deterministically.
Implementation Details
This page provides technical specifics about Theater's implementation, covering internal data structures, algorithms, and design decisions.
Actor Executor
WebAssembly Engine
Theater uses Wasmtime as its WebAssembly engine:
- Component Model Support: Uses Wasmtime's component model implementation
- Resource Management: Handles memory and table limits
- Capability Exposure: Controls which capabilities are available to components
- Asynchronous Execution: Supports async operations through poll-based approach
Host Function Implementation
Host functions are implemented using:
- WIT Bindings: Generated from WIT interface definitions
- Capability Checking: Runtime validation of permission to use functions
- Event Recording: Automatic recording of function invocations
- Resource Limiting: Constraints on resource usage
Event Chain
Data Structure
The Event Chain uses a linked data structure:
- Block Structure: Events are grouped into blocks
- Cryptographic Linking: Each block links to previous block via hash
- Content Addressing: Events and blocks are referenced by content hash
- Efficiency: Uses optimized serialization for compact representation
Persistence
Event Chain persistence strategy:
- Incremental Storage: New events are appended efficiently
- Background Compaction: Periodic optimization of storage
- Index Structures: Efficient lookup by actor, time, or event type
- Pruning Options: Configurable retention policies
Store System
Content-Addressing
Store's content-addressed architecture:
- Hash Function: SHA-256 for content identification
- Deduplication: Identical content stored only once
- Chunking Strategy: Large content split into manageable chunks
- Reference Counting: Tracks usage for garbage collection
Caching
Store's multi-level caching strategy:
- Memory Cache: Hot data kept in memory
- Local Disk Cache: Frequently accessed data on local storage
- Distributed Cache: Optional shared cache for clusters
- Prefetching: Predictive loading based on access patterns
Message Processing
Queue Implementation
Message queue architecture:
- Priority Handling: Messages can have different priorities
- Backpressure: Flow control for overloaded actors
- Delivery Guarantees: At-least-once delivery semantics
- Batching: Efficient processing of multiple messages
Concurrency Model
Approach to concurrent execution:
- Actor Isolation: Actors execute independently
- Thread Pool: Configurable worker threads for execution
- Work Stealing: Efficient distribution of processing load
- Fairness Policies: Prevent starvation of actors
Supervision System
Fault Detection
How failures are detected:
- Exception Tracking: Catches WebAssembly exceptions
- Resource Monitoring: Detects excessive resource usage
- Deadlock Detection: Identifies non-responsive actors
- Health Checks: Periodic verification of actor health
Recovery Implementation
How recovery is implemented:
- State Preservation: Maintains actor identity during restarts
- Mailbox Handling: Options for preserving or discarding pending messages
- Escalation Chain: Multi-level supervision hierarchy
- Circuit Breaking: Prevents repeated rapid failures
Networking
Protocol Design
Communication protocol details:
- Message Format: Binary protocol with versioning
- Compression: Adaptive compression based on content
- Authentication: TLS with certificate validation
- Multiplexing: Multiple logical connections over single transport
WebSocket Implementation
WebSocket support details:
- Subprotocol: Theater-specific subprotocol
- Event Streaming: Efficient real-time events
- Backpressure: Client-side flow control
- Reconnection: Automatic reconnection with session resumption
Understanding these implementation details provides insight into the technical decisions that enable Theater's unique properties and how they are realized in practice.
Actor ID System
The Theater Actor ID system provides a secure, unique identifier mechanism for all entities within the Theater ecosystem. This document covers how IDs are generated, managed, and used throughout the system.
Overview
Theater uses UUIDs (Universally Unique Identifiers) to create unique, cryptographically secure identifiers for actors and other entities. These IDs ensure:
- Uniqueness across distributed systems
- Collision resistance even in large-scale deployments
- Unpredictability for security purposes
- Consistent formatting and representation
The TheaterId Type
The core of the ID system is the TheaterId
type, which encapsulates a UUID and provides convenient methods for working with actor identifiers:
#![allow(unused)] fn main() { pub struct TheaterId(Uuid); impl TheaterId { /// Generate a new random ID pub fn generate() -> Self { Self(Uuid::new_v4()) } /// Parse a TheaterId from a string pub fn parse(s: &str) -> Result<Self, uuid::Error> { Ok(Self(Uuid::parse_str(s)?)) } /// Get the underlying UUID pub fn as_uuid(&self) -> &Uuid { &self.0 } } }
ID Generation
Actor IDs are generated using the UUID v4 format, which provides:
- 128 bits (16 bytes) of random data
- Extremely low collision probability (1 in 2^122)
- Standardized string representation
Example of generating a new actor ID:
#![allow(unused)] fn main() { let actor_id = TheaterId::generate(); }
ID String Representation
IDs are represented as standard UUID strings:
#![allow(unused)] fn main() { // Convert ID to string let id_string = actor_id.to_string(); // Format: "550e8400-e29b-41d4-a716-446655440000" // Parse string back to ID let parsed_id = TheaterId::parse("550e8400-e29b-41d4-a716-446655440000").unwrap(); }
Serialization Support
Actor IDs are designed to work seamlessly with serde for JSON serialization:
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize)] struct ActorState { id: TheaterId, // Other state fields... } }
Using Actor IDs
In Actor Manifests
Actor IDs can be referenced in manifest files:
[dependencies]
parent_actor = "550e8400-e29b-41d4-a716-446655440000"
In Message Routing
IDs are used for message routing between actors:
#![allow(unused)] fn main() { // Send a message to a specific actor by ID theater_runtime::send_message_to_actor(&target_id, message); }
In Supervision
Parent actors reference children by their IDs:
#![allow(unused)] fn main() { // Get a child actor's status let status = supervisor::get_child_status(&child_id)?; }
ID Validation
When working with IDs from external sources, always validate them:
#![allow(unused)] fn main() { match TheaterId::parse(input_string) { Ok(id) => { // Valid ID, proceed with operation }, Err(_) => { // Invalid ID, handle the error } } }
Best Practices
-
Never Generate IDs Manually
- Always use
TheaterId::generate()
to ensure proper randomness
- Always use
-
Store Full IDs
- Don't truncate or modify IDs as this reduces their uniqueness properties
-
Use Type Safety
- Prefer the
TheaterId
type over raw strings when possible - This provides compile-time guarantees and better error handling
- Prefer the
-
Handle Parse Errors
- Always check for errors when parsing IDs from strings
- Invalid IDs should be treated as authentication/authorization failures
-
Include IDs in Logs
- Log actor IDs with operations for easier debugging
- Use the string representation in log entries
Implementation Notes
- The ID system uses the
uuid
crate with thev4
feature for generation - The implementation includes comprehensive tests for generation, parsing, and serialization
- Future enhancements may include:
- Alternative ID formats for specific use cases
- ID collision detection for large-scale deployments
- Hierarchical ID systems for parent-child relationships
Planned Enhancements
Note: The following enhancements are planned for future releases:
- Secure Random ID System: A new system using 16-byte random IDs with base64url encoding
- Host CSPRNG Integration: Using the host system's cryptographically secure random number generator
- Improved Format: Shorter string representation (22 characters vs 36 for UUIDs)
- Backward Compatibility: Support for both new and legacy ID formats
Interface System
Theater's interface system is built on the WebAssembly Component Model and WebAssembly Interface Types (WIT), providing a type-safe and flexible way for actors to expose and consume functionality while maintaining verifiable state transitions.
WebAssembly Interface Types (WIT)
Theater defines its interfaces using WIT, providing a language-agnostic way to describe component interfaces. The core WIT files are located in the wit/
directory:
Core Interfaces
- actor.wit - Basic actor interface:
interface actor {
use types.{state};
init: func(state: state, params: tuple<string>) -> result<tuple<state>, string>;
}
- message-server.wit - Message handling interface:
interface message-server-client {
use types.{json, event};
handle-send: func(state: option<json>, params: tuple<json>) -> result<tuple<option<json>>, string>;
handle-request: func(state: option<json>, params: tuple<json>) -> result<tuple<option<json>, tuple<json>>, string>;
}
interface message-server-host {
use types.{json, actor-id};
send: func(actor-id: actor-id, msg: json) -> result<_, string>;
request: func(actor-id: actor-id, msg: json) -> result<json, string>;
}
- http.wit - HTTP server and client interfaces:
interface http-server {
use types.{state};
use http-types.{http-request, http-response};
handle-request: func(state: state, params: tuple<http-request>) -> result<tuple<state, tuple<http-response>>, string>;
}
interface http-client {
use types.{json};
use http-types.{http-request, http-response};
send-http: func(req: http-request) -> result<http-response, string>;
}
- supervisor.wit - Parent-child supervision:
interface supervisor {
spawn: func(manifest: string) -> result<string, string>;
list-children: func() -> list<string>;
stop-child: func(child-id: string) -> result<_, string>;
restart-child: func(child-id: string) -> result<_, string>;
get-child-state: func(child-id: string) -> result<list<u8>, string>;
get-child-events: func(child-id: string) -> result<list<chain-event>, string>;
// ...
}
- types.wit - Common data types:
interface types {
type json = list<u8>;
type state = option<list<u8>>;
type actor-id = string;
// ...
}
Handler System
Theater uses a handler system to connect actor interfaces with their implementations:
Handler Types
The current implementation includes several handler types:
-
Message Server Handler:
- Handles direct actor-to-actor messaging
- Supports both request/response and one-way sends
- Serializes messages as JSON bytes
-
HTTP Server Handler:
- Exposes actor functionality via HTTP endpoints
- Converts HTTP requests to actor messages
- Transforms responses back to HTTP
-
Supervisor Handler:
- Enables parent-child supervision
- Provides lifecycle management functions
- Access to child state and events
Handler Configuration
Handlers are configured in actor manifests:
name = "my-actor"
component_path = "my_actor.wasm"
# Message server handler
[[handlers]]
type = "message-server"
config = { port = 8080 }
interface = "ntwk:theater/message-server-client"
# HTTP server handler
[[handlers]]
type = "http-server"
config = { port = 8081 }
# Supervisor handler
[[handlers]]
type = "supervisor"
config = {}
Message Flow
Actor-to-Actor Messaging
-
Send Message (one-way):
- Sender actor calls
message-server-host::send
- Message is routed through TheaterRuntime
- Recipient actor's
handle-send
is called - State is updated and recorded in hash chain
- No response is returned to sender
- Sender actor calls
-
Request Message (request/response):
- Sender actor calls
message-server-host::request
- Message is routed through TheaterRuntime
- Recipient actor's
handle-request
is called - State is updated and recorded in hash chain
- Response is returned to sender
- Sender actor calls
HTTP Integration
-
Incoming HTTP Request:
- HTTP request arrives at server
- Request is converted to
http-request
struct - Actor's
handle-request
function is called - Response is converted back to HTTP and returned
-
Outgoing HTTP Request:
- Actor calls
http-client::send-http
- Request is made to external service
- Response is returned to actor
- Interaction is recorded in hash chain
- Actor calls
Interface Implementation
Actors implement interfaces through WebAssembly components:
Required Component Structure
A Theater actor component must:
- Implement required interfaces (based on handlers)
- Export interface functions with correct signatures
- Handle state consistently
- Process messages according to interface specifications
Example Actor Implementation
#![allow(unused)] fn main() { use theater_sdk::{actor, message_server}; struct CounterActor; #[actor::export] impl actor::Actor for CounterActor { fn init(state: Option<Vec<u8>>, params: (String,)) -> Result<(Option<Vec<u8>>,), String> { // Initialize with either existing state or new state let state = state.unwrap_or_else(|| { let initial_state = serde_json::json!({ "count": 0 }); serde_json::to_vec(&initial_state).unwrap() }); Ok((Some(state),)) } } #[message_server::export] impl message_server::MessageServerClient for CounterActor { fn handle_send( state: Option<Vec<u8>>, params: (Vec<u8>,) ) -> Result<(Option<Vec<u8>>,), String> { // Process one-way message // ... Ok((new_state,)) } fn handle_request( state: Option<Vec<u8>>, params: (Vec<u8>,) ) -> Result<(Option<Vec<u8>>, (Vec<u8>,)), String> { // Process request/response message // ... Ok((new_state, (response,))) } } }
Working with State
The interface system consistently handles state:
-
State Representation:
- State is represented as
Option<Vec<u8>>
(optional bytes) - Typically contains serialized JSON or other format
- State is passed to and from interface functions
- State is represented as
-
State Updates:
- Functions return new state
- Changes are recorded in hash chain
- State is available for inspection and verification
-
State Access:
- Current state is provided to interface functions
- Functions can modify state by returning new version
- Parent actors can access child state via supervision
Actor Manifest
The manifest connects interfaces to implementations:
name = "counter-actor"
component_path = "counter.wasm"
# Interfaces implemented by this actor
[interface]
implements = [
"ntwk:theater/actor",
"ntwk:theater/message-server-client",
"ntwk:theater/http-server"
]
# Interfaces required by this actor
requires = [
"ntwk:theater/message-server-host"
]
# Message server handler
[[handlers]]
type = "message-server"
config = {}
# HTTP server handler
[[handlers]]
type = "http-server"
config = { port = 8080 }
Interface Composition
Theater's interface system is designed for composition, allowing actors to:
-
Implement Multiple Interfaces:
- Core actor functionality
- Message handling
- HTTP serving
- Custom functionality
-
Depend on Host Interfaces:
- Message sending
- HTTP client
- Supervision
- File system access
-
Combine Interface Types:
- One interface can extend another
- Interfaces can share common types
- Versioning through interface namespaces
Each interface maintains state chain integrity while providing a specific capability.
Message Structure
While the interface system is flexible, messages typically follow a standard structure:
{
"type": "request_type",
"action": "specific_operation",
"payload": {
"param1": "value1",
"param2": 42
},
"metadata": {
"timestamp": "2025-02-26T12:34:56Z",
"request_id": "req-123456"
}
}
Responses typically include:
{
"type": "response",
"status": "success",
"payload": {
"result": "value"
},
"metadata": {
"timestamp": "2025-02-26T12:34:57Z",
"request_id": "req-123456"
}
}
Debugging Interfaces
Theater provides several mechanisms for debugging interfaces:
-
Tracing:
- All interface calls are logged
- State transitions are recorded
- Message flow can be traced end-to-end
-
Interface Inspection:
- WIT interfaces can be introspected
- Available functions can be listed
- Type checking for message formats
-
State Verification:
- Hash chain can be verified at any point
- State history can be examined
- State transitions can be replayed
Custom Interface Development
Creating new interfaces requires:
-
WIT Definition:
- Define interface functions and types
- Document expected behavior
- Specify state handling patterns
-
Handler Implementation:
- Create handler in Theater runtime
- Connect WIT interface to actor
- Handle message routing correctly
-
Actor Implementation:
- Implement interface functions
- Handle state properly
- Process messages according to spec
Best Practices
-
Interface Design
- Keep interfaces focused on single responsibility
- Use clear, descriptive function names
- Document expected behavior
- Provide meaningful error messages
- Consider versioning strategy
-
Message Design
- Use consistent type field for categorization
- Include action field for specific operations
- Structure payloads logically
- Add metadata for debugging
- Handle errors consistently
-
State Management
- Keep state serializable
- Handle state transitions atomically
- Validate state after changes
- Consider state size impacts
- Test state rollback scenarios
-
Security Considerations
- Validate all input messages
- Sanitize data crossing interface boundaries
- Control access to sensitive interfaces
- Verify state integrity frequently
- Test for message injection risks
Theater Handlers
Handlers are the primary way actors interact with the outside world and with each other in Theater. This section provides an overview and links to detailed documentation for each handler type.
Handler System Overview
The Handler System documentation provides a comprehensive overview of how handlers work in Theater, including:
- What handlers are and their role in the Theater architecture
- How handlers connect actors to the outside world and with other actors
- The distinction between "host" functions (imports) and "export" functions
- The handler lifecycle within the actor runtime
- How handlers are configured in manifests
Available Handlers
Theater provides several built-in handlers that enable different capabilities:
Message Server Handler
The Message Server Handler is the primary mechanism for actor-to-actor communication, enabling:
- One-way message sending
- Request-response patterns
- Channel-based communication
HTTP Client Handler
The HTTP Client Handler allows actors to make HTTP requests to external services, with:
- Support for all HTTP methods
- Header and body customization
- Automatic state chain recording
HTTP Framework Handler
The HTTP Framework Handler exposes actor functionality via HTTP endpoints, enabling:
- HTTP server capabilities
- RESTful API development
- Web service creation
Filesystem Handler
The Filesystem Handler provides actors with controlled access to the local filesystem for:
- Reading and writing files
- Directory operations
- File metadata access
Supervisor Handler
The Supervisor Handler enables parent-child relationships between actors, supporting:
- Spawning child actors
- Lifecycle management
- Supervision strategies
Store Handler
The Store Handler provides access to Theater's content-addressable storage system for:
- Content-addressed storage
- Label management
- Persistent data storage
Runtime Handler
The Runtime Handler provides information about and control over the actor's runtime environment:
- System information
- Environment variables
- Logging and metrics
Timing Handler
The Timing Handler provides time-related capabilities:
- Controlled delays
- Timeout patterns
- High-resolution timing
Next Steps
Choose a handler from the list above to learn more about its capabilities, configuration options, and usage patterns.
Handler System
The handler system is the core of how actors interact with the outside world and with each other in Theater. Handlers provide the bridge between WebAssembly actors and host capabilities, enabling actors to send messages, access resources, and participate in supervision hierarchies.
What Are Handlers?
Handlers are specialized components that:
- Connect Actors with Host Capabilities: Handlers expose host functions to WebAssembly actors, allowing them to interact with the host system and other actors
- Process Messages and Events: Handlers receive and process incoming messages, translating them into actor function calls
- Maintain Chain Integrity: All handler operations are recorded in the actor's chain, maintaining the verifiable history
- Provide Standard Interfaces: Handlers implement standard WebAssembly Interface Type (WIT) interfaces, ensuring consistency across actors
Handler Architecture
Each handler consists of two main parts:
-
Host Functions (Imports): Functions provided by the host environment that actors can call. These are integrated into the actor's WebAssembly module during instantiation.
-
Exported Functions (Exports): Functions that the actor implements and the handler calls in response to external events or messages.
This bidirectional interface allows for complete interaction patterns while maintaining the security boundaries provided by WebAssembly.
Handler Lifecycle
When an actor is started:
- Registration: Handlers specified in the actor's manifest are registered with the actor runtime
- Initialization: Each handler is initialized and connected to the actor component
- Host Function Setup: The handler adds its host functions to the actor's WebAssembly linker
- Export Function Registration: The handler registers the actor's exported functions for callbacks
- Start: The handler's event loop is started in a separate task
During operation:
- Message Processing: Handlers receive messages or events and process them
- Function Calls: Handlers call actor functions or respond to actor requests to host functions
- State Recording: All interactions are recorded in the actor's state chain
During shutdown:
- Graceful Termination: Handlers receive shutdown signals and perform cleanup
- Resource Release: All resources owned by handlers are released
Handler Configuration
Handlers are configured in the actor's manifest file (TOML format):
name = "my-actor"
component_path = "my_actor.wasm"
[[handlers]]
type = "message-server"
config = {}
[[handlers]]
type = "http-client"
config = {}
[[handlers]]
type = "filesystem"
config = { new_dir = true }
Each handler entry includes:
type
: The handler type identifierconfig
: Handler-specific configuration options
Available Handler Types
Theater provides several built-in handler types:
- message-server: Enables actor-to-actor messaging using both synchronous (request/response) and asynchronous (fire-and-forget) patterns
- http-client: Allows actors to make HTTP requests to external services
- http-framework: Exposes actor functionality via HTTP endpoints
- filesystem: Provides access to the filesystem with appropriate sandboxing
- supervisor: Enables parent-child actor relationships for supervision
- store: Provides content-addressable storage for actors
- runtime: Gives access to runtime information and operations
- timing: Provides timing and scheduling capabilities
WebAssembly Interface Types (WIT)
Handler abilitiesare exposed to the actors using WebAssembly Interface Types (WIT), which provide a language-agnostic way to describe component interfaces. For example, the message-server interface is defined as:
interface message-server-client {
use types.{json, event};
handle-send: func(state: option<json>, params: tuple<json>) -> result<tuple<option<json>>, string>;
handle-request: func(state: option<json>, params: tuple<json>) -> result<tuple<option<json>, tuple<json>>, string>;
}
interface message-server-host {
use types.{json, actor-id};
send: func(actor-id: actor-id, msg: json) -> result<_, string>;
request: func(actor-id: actor-id, msg: json) -> result<json, string>;
}
Handler Implementation Details
Under the hood, handlers are implemented as Rust structs that:
- Implement the
Handler
trait - Handle setup of host functions
- Process messages and events
- Call actor functions when needed
- Maintain appropriate state
For example, the MessageServerHost
implements handler functionality for actor-to-actor messaging.
Handler Security Model
The handler system is designed with security in mind:
- Sandboxed Access: Handlers provide controlled access to host resources
- Verifiable State: All handler operations are recorded in the chain
- Explicit Permissions: Actors must explicitly declare which handlers they use
Developing Custom Handlers
To develop a new handler for Theater:
- Define the WIT Interface: Create a new
.wit
file defining the interface - Implement the Handler: Create a new handler implementation in Rust
- Register the Handler: Add the handler to the configuration system
- Connect to Actor Runtime: Integrate the handler with the actor runtime
Best Practices
- Handler Selection: Only include handlers that your actor actually needs
- Resource Management: Configure appropriate resource limits for handlers
- Error Handling: Implement proper error handling for handler operations
- Testing: Test handlers in isolation before integrating them
Next Steps
In the following sections, we'll explore each handler type in detail, including:
- Specific configuration options
- Available functions
- Usage patterns
- Examples
See the individual handler documentation for more details:
- Message Server Handler
- HTTP Client Handler
- HTTP Framework Handler
- File System Handler
- Supervisor Handler
- Store Handler
- Runtime Handler
- Timing Handler
Message Server Handler
The Message Server Handler is the primary mechanism for actor-to-actor communication in Theater. It enables actors to send messages to each other, establish request-response patterns, and create persistent communication channels.
Overview
The Message Server Handler implements two key interfaces:
- message-server-host: Functions that actors can call to send messages to other actors
- message-server-client: Functions that actors implement to receive and process messages
Together, these interfaces enable a complete messaging system within the Theater ecosystem.
Configuration
The Message Server Handler requires minimal configuration in the actor's manifest:
[[handlers]]
type = "message-server"
config = {}
The handler is automatically added to all actors, so you don't need to explicitly include it in your manifest.
Messaging Patterns
The Message Server Handler supports three primary communication patterns:
1. One-Way Messages (Send)
Send messages are "fire-and-forget" - the sender doesn't wait for a response.
Host Interface (actor calling):
send: func(actor-id: actor-id, msg: json) -> result<_, string>;
Client Interface (actor implementing):
handle-send: func(state: option<json>, params: tuple<json>) -> result<tuple<option<json>>, string>;
Usage Example:
#![allow(unused)] fn main() { // Sending a message message_server_host::send(target_id, message_data)?; // Handling a message fn handle_send(state: Option<Vec<u8>>, params: (Vec<u8>,)) -> Result<(Option<Vec<u8>>,), String> { // Process message and update state Ok((new_state,)) } }
2. Request-Response Messages
Request messages expect a response from the recipient.
Host Interface (actor calling):
request: func(actor-id: actor-id, msg: json) -> result<json, string>;
Client Interface (actor implementing):
handle-request: func(state: option<json>, params: tuple<json>) -> result<tuple<option<json>, tuple<json>>, string>;
Usage Example:
#![allow(unused)] fn main() { // Sending a request let response = message_server_host::request(target_id, request_data)?; // Handling a request fn handle_request(state: Option<Vec<u8>>, params: (Vec<u8>,)) -> Result<(Option<Vec<u8>>, (Vec<u8>,)), String> { // Process request, update state, and prepare response Ok((new_state, (response,))) } }
3. Channel-Based Communication
Channels provide a persistent communication pathway between actors.
Host Interface (actor calling):
open-channel: func(actor-id: actor-id, initial-msg: json) -> result<string, string>;
send-on-channel: func(channel-id: string, msg: json) -> result<_, string>;
close-channel: func(channel-id: string) -> result<_, string>;
Client Interface (actor implementing):
handle-channel-open: func(state: option<json>, params: tuple<json>) -> result<tuple<option<json>, tuple<channel-accept>>, string>;
handle-channel-message: func(state: option<json>, params: tuple<string, json>) -> result<tuple<option<json>>, string>;
handle-channel-close: func(state: option<json>, params: tuple<string>) -> result<tuple<option<json>>, string>;
Usage Example:
#![allow(unused)] fn main() { // Opening a channel let channel_id = message_server_host::open_channel(target_id, initial_message)?; // Sending on a channel message_server_host::send_on_channel(channel_id, message_data)?; // Closing a channel message_server_host::close_channel(channel_id)?; // Handling channel operations fn handle_channel_open(state: Option<Vec<u8>>, params: (Vec<u8>,)) -> Result<(Option<Vec<u8>>, (ChannelAccept,)), String> { // Process channel open request and decide whether to accept Ok((new_state, (channel_accept,))) } }
Message Format
Messages are typically serialized as JSON bytes with a standard structure:
{
"type": "message_type",
"action": "specific_action",
"payload": {
"key1": "value1",
"key2": 42
},
"metadata": {
"timestamp": "2025-03-20T12:34:56Z",
"message_id": "msg-12345"
}
}
State Management
Every message operation is recorded in the actor's state chain, maintaining a verifiable history of all communications. The event data includes:
- Message type (send, request, channel)
- Timestamp
- Recipient information
- Success/failure status
- Message size
Error Handling
The Message Server Handler provides detailed error information for various failure scenarios:
- Invalid Actor ID: When the target actor doesn't exist
- Delivery Failure: When the message can't be delivered
- Processing Error: When the target actor fails to process the message
- Channel Errors: When channel operations fail (non-existent channel, closed channel)
All errors are properly recorded in the state chain for debugging.
Implementation Details
The Message Server Handler processes messages through a dedicated task that:
- Receives incoming messages from the actor's mailbox
- Processes messages based on their type
- Calls the appropriate actor function
- Updates the actor's state
- Returns responses (for request messages)
- Records all operations in the state chain
Channel Management
Channels have additional lifecycle management:
- Channel Creation: A unique channel ID is generated for each channel
- Channel Acceptance: The target actor must explicitly accept the channel
- Channel State: The handler tracks which channels are open
- Channel Closure: Either participant can close the channel
Best Practices
- Message Structure: Use consistent message structures with clear type and action fields
- Error Handling: Always handle potential errors from message operations
- Channel Management: Close channels when they're no longer needed
- State Design: Keep message handlers focused on state transitions
- Timeout Handling: Consider implementing timeouts for request operations
Security Considerations
- Message Validation: Always validate incoming messages before processing
- Actor ID Verification: Verify actor IDs before sending messages
- Payload Size: Be mindful of message payload sizes
- Error Exposure: Don't expose sensitive information in error messages
Examples
Example 1: Simple Request-Response
#![allow(unused)] fn main() { // Actor sending a request pub fn get_data_from_actor(target_id: &str) -> Result<Data, String> { let request = serde_json::json!({ "type": "request", "action": "get_data", "payload": {} }); let request_bytes = serde_json::to_vec(&request).unwrap(); let response_bytes = message_server_host::request(target_id, request_bytes)?; let response: Data = serde_json::from_slice(&response_bytes).unwrap(); Ok(response) } // Actor handling the request fn handle_request(state: Option<Vec<u8>>, params: (Vec<u8>,)) -> Result<(Option<Vec<u8>>, (Vec<u8>,)), String> { let message: serde_json::Value = serde_json::from_slice(¶ms.0).unwrap(); if message["action"] == "get_data" { let data = serde_json::to_vec(&get_data()).unwrap(); Ok((state, (data,))) } else { Err("Unknown action".to_string()) } } }
Example 2: Channel Communication
#![allow(unused)] fn main() { // Actor opening a channel pub fn open_data_stream(target_id: &str) -> Result<String, String> { let initial_message = serde_json::json!({ "type": "channel", "action": "open_data_stream", "payload": { "frequency": "1s" } }); let message_bytes = serde_json::to_vec(&initial_message).unwrap(); let channel_id = message_server_host::open_channel(target_id, message_bytes)?; Ok(channel_id) } // Actor handling channel open fn handle_channel_open(state: Option<Vec<u8>>, params: (Vec<u8>,)) -> Result<(Option<Vec<u8>>, (ChannelAccept,)), String> { let message: serde_json::Value = serde_json::from_slice(¶ms.0).unwrap(); if message["action"] == "open_data_stream" { // Accept the channel let channel_accept = ChannelAccept { accepted: true, message: None, }; Ok((state, (channel_accept,))) } else { // Reject the channel let channel_accept = ChannelAccept { accepted: false, message: Some(b"Unsupported action".to_vec()), }; Ok((state, (channel_accept,))) } } }
Related Topics
- HTTP Framework Handler - For HTTP-based communication
- Supervisor Handler - For parent-child communication
- State Management - For understanding state chain integration
HTTP Client Handler
The HTTP Client Handler enables actors to make HTTP requests to external services while maintaining Theater's state verification and security principles. This handler allows actors to interact with external APIs, fetch resources, and communicate with web services.
Overview
The HTTP Client Handler implements the ntwk:theater/http-client
interface, providing a way for actors to:
- Send HTTP requests to external services
- Process HTTP responses
- Record all HTTP interactions in the state chain
- Handle errors in a consistent way
Configuration
To use the HTTP Client Handler, add it to your actor's manifest:
[[handlers]]
type = "http-client"
config = {}
Currently, the HTTP Client Handler doesn't require any specific configuration parameters.
Interface
The HTTP Client Handler is defined using the following WIT interface:
interface http-client {
use types.{json};
use http-types.{http-request, http-response};
send-http: func(req: http-request) -> result<http-response, string>;
}
HTTP Request Structure
The HttpRequest
type has the following structure:
#![allow(unused)] fn main() { struct HttpRequest { method: String, uri: String, headers: Vec<(String, String)>, body: Option<Vec<u8>>, } }
method
: The HTTP method (GET, POST, PUT, DELETE, etc.)uri
: The target URLheaders
: A list of HTTP headers as key-value pairsbody
: Optional request body as bytes
HTTP Response Structure
The HttpResponse
type has the following structure:
#![allow(unused)] fn main() { struct HttpResponse { status: u16, headers: Vec<(String, String)>, body: Option<Vec<u8>>, } }
status
: The HTTP status codeheaders
: A list of response headers as key-value pairsbody
: Optional response body as bytes
Making HTTP Requests
To make an HTTP request, actors call the send-http
function with an HttpRequest
object:
#![allow(unused)] fn main() { let request = HttpRequest { method: "GET".to_string(), uri: "https://api.example.com/data".to_string(), headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ("Authorization".to_string(), "Bearer token123".to_string()), ], body: None, }; match http_client::send_http(request) { Ok(response) => { // Process response println!("Status: {}", response.status); if let Some(body) = response.body { // Handle response body } }, Err(error) => { // Handle error println!("Request failed: {}", error); } } }
State Chain Integration
Every HTTP request and response is recorded in the actor's state chain, creating a verifiable history of all external interactions. The chain events include:
-
HttpClientRequestCall: Records when a request is made, including:
- HTTP method
- Target URL
- Headers count
- Body size
-
HttpClientRequestResult: Records the result of a request, including:
- Status code
- Headers count
- Body size
- Success indicator
-
Error: Records any errors that occur during the request, including:
- Operation type
- URL path
- Error message
This state chain integration ensures that all external interactions are:
- Traceable
- Verifiable
- Reproducible
- Auditable
Error Handling
The HTTP Client Handler provides detailed error information for various failure scenarios:
- Invalid Method: When an invalid HTTP method is specified
- Network Errors: When network issues prevent the request from completing
- Timeout Errors: When the request times out
- Parser Errors: When response parsing fails
All errors are returned as strings and are also recorded in the state chain.
Security Considerations
When using the HTTP Client Handler, consider the following security aspects:
- URL Validation: Validate URLs before making requests to prevent SSRF attacks
- Sensitive Data: Be careful with sensitive data in requests, as they are recorded in the state chain
- Authentication: Use secure methods for authentication in external APIs
- TLS Verification: The handler performs TLS verification by default
- Timeouts: Set appropriate timeouts for requests to prevent resource exhaustion
Implementation Details
Under the hood, the HTTP Client Handler:
- Converts the
HttpRequest
into a reqwest client request - Sets up headers, body, and method
- Executes the request asynchronously
- Processes the response into an
HttpResponse
- Records all operations in the state chain
- Returns the response or error to the actor
The handler uses the reqwest crate for HTTP functionality, providing a robust and well-tested HTTP client implementation.
Limitations
The current HTTP Client Handler implementation has some limitations:
- No Direct Streaming: Large responses are loaded fully into memory
- No WebSocket Support: For WebSocket connections, use a dedicated WebSocket client
- No Client Certificate Authentication: TLS client certificates are not currently supported
- No Direct Proxy Configuration: Proxy settings cannot be configured per-request
Best Practices
- Error Handling: Always handle errors from HTTP requests properly
- Response Size: Be mindful of response sizes to avoid memory issues
- Request Rate: Implement rate limiting for external API calls
- Timeout Handling: Set appropriate timeouts for your use case
- Idempotency: Design requests to be idempotent when possible
- Retries: Implement retry logic for transient failures
Examples
Example 1: Simple GET Request
#![allow(unused)] fn main() { pub fn fetch_json_data() -> Result<serde_json::Value, String> { let request = HttpRequest { method: "GET".to_string(), uri: "https://api.example.com/data.json".to_string(), headers: vec![("Accept".to_string(), "application/json".to_string())], body: None, }; let response = http_client::send_http(request)?; if response.status != 200 { return Err(format!("API returned status code: {}", response.status)); } if let Some(body) = response.body { let json = serde_json::from_slice(&body) .map_err(|e| format!("Failed to parse JSON: {}", e))?; Ok(json) } else { Err("Response body was empty".to_string()) } } }
Example 2: POST Request with JSON Body
#![allow(unused)] fn main() { pub fn create_resource(data: &CreateResourceRequest) -> Result<ResourceResponse, String> { let json_body = serde_json::to_vec(data) .map_err(|e| format!("Failed to serialize request: {}", e))?; let request = HttpRequest { method: "POST".to_string(), uri: "https://api.example.com/resources".to_string(), headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ("Authorization".to_string(), format!("Bearer {}", get_token())), ], body: Some(json_body), }; let response = http_client::send_http(request)?; match response.status { 201 => { // Resource created successfully if let Some(body) = response.body { let resource: ResourceResponse = serde_json::from_slice(&body) .map_err(|e| format!("Failed to parse response: {}", e))?; Ok(resource) } else { Err("Response body was empty".to_string()) } }, 400..=499 => { // Client error Err(format!("Client error: {}", response.status)) }, 500..=599 => { // Server error Err(format!("Server error: {}", response.status)) }, _ => { // Unexpected status code Err(format!("Unexpected status code: {}", response.status)) } } } }
Example 3: File Download
#![allow(unused)] fn main() { pub fn download_file(url: &str) -> Result<Vec<u8>, String> { let request = HttpRequest { method: "GET".to_string(), uri: url.to_string(), headers: vec![], body: None, }; let response = http_client::send_http(request)?; if response.status != 200 { return Err(format!("Download failed with status: {}", response.status)); } if let Some(body) = response.body { Ok(body) } else { Err("Download resulted in empty file".to_string()) } } }
Related Topics
- HTTP Framework Handler - For creating HTTP servers
- Store Handler - For storing downloaded content
- Message Server Handler - For actor-to-actor communication
HTTP Framework Handler
The HTTP Framework Handler enables actors to serve HTTP requests, turning them into fully-functional web services. It provides a bridge between incoming HTTP requests and actor functions, allowing actors to respond to web traffic while maintaining Theater's state verification model.
Overview
The HTTP Framework Handler implements the ntwk:theater/http-framework
interface, providing:
- A way for actors to receive and respond to HTTP requests
- Conversion between HTTP requests and actor-friendly formats
- Automatic state chain recording of all HTTP interactions
- Comprehensive error handling
Configuration
To use the HTTP Framework Handler, add it to your actor's manifest:
[[handlers]]
type = "http-framework"
config = {}
The HTTP Framework Handler works in conjunction with the built-in HTTP server capability in Theater, which routes requests to the appropriate actors based on path configurations.
Interface
The HTTP Framework Handler is defined using the following WIT interface:
interface http-framework {
use types.{state};
use http-types.{http-request, http-response};
handle-request: func(state: state, req: http-request) -> result<tuple<state, http-response>, string>;
}
HTTP Request Structure
The HttpRequest
type has the following structure:
#![allow(unused)] fn main() { struct HttpRequest { method: String, uri: String, path: String, query: Option<String>, headers: Vec<(String, String)>, body: Option<Vec<u8>>, } }
method
: The HTTP method (GET, POST, PUT, DELETE, etc.)uri
: The full request URIpath
: The path component of the URIquery
: Optional query stringheaders
: A list of HTTP headers as key-value pairsbody
: Optional request body as bytes
HTTP Response Structure
The HttpResponse
type has the following structure:
#![allow(unused)] fn main() { struct HttpResponse { status: u16, headers: Vec<(String, String)>, body: Option<Vec<u8>>, } }
status
: The HTTP status codeheaders
: A list of response headers as key-value pairsbody
: Optional response body as bytes
Handling HTTP Requests
To handle HTTP requests, actors implement the handle-request
function:
#![allow(unused)] fn main() { fn handle_request(state: Option<Vec<u8>>, req: HttpRequest) -> Result<(Option<Vec<u8>>, HttpResponse), String> { // Process the request and update state let new_state = process_request(&state, &req)?; // Generate a response let response = HttpResponse { status: 200, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ], body: Some(b"Hello, world!".to_vec()), }; Ok((new_state, response)) } }
Routing
The HTTP Framework Handler maps incoming HTTP requests to actor functions based on the request path. This is configured through the Theater system's HTTP server configuration.
For example, to route all requests to /api/users
to a specific actor:
# System configuration
[[http_routes]]
path = "/api/users"
actor_id = "user-service-actor"
# In the actor's manifest
[[handlers]]
type = "http-framework"
config = {}
State Chain Integration
Every HTTP request and response is recorded in the actor's state chain, creating a verifiable history of all web interactions. The chain events include:
-
HttpFrameworkRequestCall: Records when a request is received, including:
- HTTP method
- Path
- Headers count
- Body size
-
HttpFrameworkRequestResult: Records the result of processing a request, including:
- Status code
- Headers count
- Body size
- Processing time
-
Error: Records any errors that occur during request processing, including:
- Operation type
- Path
- Error message
Error Handling
The HTTP Framework Handler provides two layers of error handling:
-
Framework-Level Errors: Handled by the framework itself, such as:
- Routing errors
- Method not allowed
- Actor not found
- Malformed requests
-
Actor-Level Errors: Returned by the actor's
handle-request
function, which can:- Return a custom error response
- Provide detailed error information
- Choose appropriate HTTP status codes
If an actor returns an error, the framework generates a 500 Internal Server Error response with the error message in the body (in development mode only).
Security Considerations
When using the HTTP Framework Handler, consider the following security aspects:
- Input Validation: Always validate and sanitize all HTTP request data
- Authentication: Implement proper authentication for protected endpoints
- Rate Limiting: Consider rate limiting to prevent abuse
- Error Information: Be careful about exposing error details in production
- CORS Policies: Implement appropriate CORS headers for browser security
- Content Security: Set proper content security policies
Implementation Details
Under the hood, the HTTP Framework Handler:
- Receives HTTP requests from the Theater HTTP server
- Converts them to the
HttpRequest
format - Retrieves the current actor state
- Calls the actor's
handle-request
function - Updates the actor's state with the new state
- Converts the
HttpResponse
back to an HTTP response - Records all operations in the state chain
- Returns the response to the client
Best Practices
- RESTful Design: Follow RESTful principles for API design
- Stateless Design: Keep HTTP handlers as stateless as possible
- Error Handling: Implement proper error handling with appropriate status codes
- Content Types: Set appropriate Content-Type headers
- Validation: Validate all incoming data
- Testing: Test all endpoints with various input scenarios
- Documentation: Document your API endpoints clearly
Examples
Example 1: Simple JSON API
#![allow(unused)] fn main() { fn handle_request(state: Option<Vec<u8>>, req: HttpRequest) -> Result<(Option<Vec<u8>>, HttpResponse), String> { // Parse the current state or initialize it let current_state: AppState = match state { Some(data) => serde_json::from_slice(&data).map_err(|e| e.to_string())?, None => AppState::default(), }; match (req.method.as_str(), req.path.as_str()) { ("GET", "/api/items") => { // Return all items let items_json = serde_json::to_vec(¤t_state.items).map_err(|e| e.to_string())?; Ok(( state, HttpResponse { status: 200, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ], body: Some(items_json), } )) }, ("POST", "/api/items") => { // Add a new item if let Some(body) = req.body { let new_item: Item = serde_json::from_slice(&body).map_err(|e| e.to_string())?; // Update state let mut new_state = current_state.clone(); new_state.items.push(new_item); // Serialize new state let new_state_bytes = serde_json::to_vec(&new_state).map_err(|e| e.to_string())?; // Return success response Ok(( Some(new_state_bytes), HttpResponse { status: 201, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ], body: Some(b"{\"status\":\"created\"}".to_vec()), } )) } else { // Return error for missing body Ok(( state, HttpResponse { status: 400, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ], body: Some(b"{\"error\":\"Missing request body\"}".to_vec()), } )) } }, _ => { // Return 404 for unmatched routes Ok(( state, HttpResponse { status: 404, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()), ], body: Some(b"{\"error\":\"Not found\"}".to_vec()), } )) } } } }
Example 2: File Serving
#![allow(unused)] fn main() { fn handle_request(state: Option<Vec<u8>>, req: HttpRequest) -> Result<(Option<Vec<u8>>, HttpResponse), String> { // Only handle GET requests if req.method != "GET" { return Ok(( state, HttpResponse { status: 405, headers: vec![ ("Content-Type".to_string(), "text/plain".to_string()), ("Allow".to_string(), "GET".to_string()), ], body: Some(b"Method Not Allowed".to_vec()), } )); } // Extract the requested file path let path = req.path.trim_start_matches('/'); // Use the filesystem handler to read the file match filesystem::read_file(path) { Ok(file_content) => { // Determine content type based on file extension let content_type = match path.split('.').last() { Some("html") => "text/html", Some("css") => "text/css", Some("js") => "application/javascript", Some("json") => "application/json", Some("png") => "image/png", Some("jpg") | Some("jpeg") => "image/jpeg", Some("svg") => "image/svg+xml", _ => "application/octet-stream", }; // Return the file content Ok(( state, HttpResponse { status: 200, headers: vec![ ("Content-Type".to_string(), content_type.to_string()), ], body: Some(file_content), } )) }, Err(_) => { // File not found Ok(( state, HttpResponse { status: 404, headers: vec![ ("Content-Type".to_string(), "text/plain".to_string()), ], body: Some(b"File Not Found".to_vec()), } )) } } } }
Related Topics
- HTTP Client Handler - For making HTTP requests from actors
- Message Server Handler - For actor-to-actor communication
- File System Handler - For accessing the file system
Filesystem Handler
The Filesystem Handler provides actors with controlled access to the local filesystem. It enables reading and writing files, directory operations, and file metadata access, all while maintaining the Theater security model and state verification.
Overview
The Filesystem Handler implements the ntwk:theater/filesystem
interface, providing actors with the ability to:
- Read and write files securely
- Create and manage directories
- Get file metadata
- List directory contents
- Safely access files within a specified path boundary
Configuration
To use the Filesystem Handler, add it to your actor's manifest:
[[handlers]]
type = "filesystem"
config = {
path = "data/my-actor",
allowed_commands = ["read", "write"]
}
Configuration options:
path
: (Optional) The base directory for all file operations, restricting access to this directory and its subdirectoriesnew_dir
: (Optional) Iftrue
, creates a new directory in /tmp/theater for the actor; iffalse
, uses the specified path directlyallowed_commands
: (Optional) List of allowed filesystem operations; if not specified, all operations are allowed
Interface
The Filesystem Handler is defined using the following WIT interface:
interface filesystem {
read-file: func(path: string) -> result<list<u8>, string>;
write-file: func(path: string, content: list<u8>) -> result<_, string>;
append-file: func(path: string, content: list<u8>) -> result<_, string>;
exists: func(path: string) -> result<bool, string>;
is-file: func(path: string) -> result<bool, string>;
is-dir: func(path: string) -> result<bool, string>;
create-dir: func(path: string) -> result<_, string>;
remove-file: func(path: string) -> result<_, string>;
remove-dir: func(path: string) -> result<_, string>;
list-dir: func(path: string) -> result<list<file-entry>, string>;
metadata: func(path: string) -> result<file-metadata, string>;
record file-entry {
name: string,
is-file: bool,
is-dir: bool,
}
record file-metadata {
name: string,
path: string,
size: u64,
is-file: bool,
is-dir: bool,
created: option<u64>,
modified: option<u64>,
accessed: option<u64>,
}
}
File Operations
Reading Files
To read a file:
#![allow(unused)] fn main() { match filesystem::read_file("config.json") { Ok(content) => { // Process file content let config: Config = serde_json::from_slice(&content).unwrap(); // ... }, Err(error) => { // Handle error println!("Failed to read file: {}", error); } } }
Writing Files
To write a file:
#![allow(unused)] fn main() { let data = serde_json::to_vec(&config).unwrap(); match filesystem::write_file("config.json", data) { Ok(_) => { // File written successfully }, Err(error) => { // Handle error println!("Failed to write file: {}", error); } } }
Appending to Files
To append to a file:
#![allow(unused)] fn main() { let log_entry = format!("[{}] User logged in\n", get_timestamp()); match filesystem::append_file("logs/app.log", log_entry.into_bytes()) { Ok(_) => { // Log entry added successfully }, Err(error) => { // Handle error println!("Failed to append to log: {}", error); } } }
Directory Operations
Creating Directories
To create a directory:
#![allow(unused)] fn main() { match filesystem::create_dir("data/uploads") { Ok(_) => { // Directory created successfully }, Err(error) => { // Handle error println!("Failed to create directory: {}", error); } } }
Listing Directory Contents
To list directory contents:
#![allow(unused)] fn main() { match filesystem::list_dir("data") { Ok(entries) => { for entry in entries { println!( "{}: {}", if entry.is_file { "FILE" } else { "DIR " }, entry.name ); } }, Err(error) => { // Handle error println!("Failed to list directory: {}", error); } } }
File Information
Checking if a File Exists
To check if a file or directory exists:
#![allow(unused)] fn main() { match filesystem::exists("config.json") { Ok(exists) => { if exists { // File exists, proceed with operation } else { // File doesn't exist, handle accordingly } }, Err(error) => { // Handle error println!("Failed to check file existence: {}", error); } } }
Getting File Metadata
To get file metadata:
#![allow(unused)] fn main() { match filesystem::metadata("data/file.txt") { Ok(metadata) => { println!("Name: {}", metadata.name); println!("Size: {} bytes", metadata.size); println!("Is file: {}", metadata.is_file); println!("Is directory: {}", metadata.is_dir); if let Some(created) = metadata.created { println!("Created: {}", format_timestamp(created)); } if let Some(modified) = metadata.modified { println!("Modified: {}", format_timestamp(modified)); } }, Err(error) => { // Handle error println!("Failed to get file metadata: {}", error); } } }
Path Resolution
All paths in the Filesystem Handler are resolved relative to the base directory specified in the configuration. This provides a security boundary that prevents actors from accessing files outside their designated area.
For example, if the base directory is configured as data/my-actor
:
#![allow(unused)] fn main() { // Actual path: data/my-actor/config.json filesystem::read_file("config.json"); // Actual path: data/my-actor/logs/app.log filesystem::write_file("logs/app.log", content); }
Attempts to access files outside this boundary using path traversal (e.g., ../other-actor/file.txt
) will be blocked by the handler.
State Chain Integration
All filesystem operations are recorded in the actor's state chain, creating a verifiable history. The chain events include:
-
FilesystemOperation: Records details of the operation:
- Operation type (read, write, list, etc.)
- Path
- Size (for read/write operations)
- Success/failure status
-
Error: Records any errors that occur:
- Operation type
- Path
- Error message
This integration ensures that all file interactions are:
- Traceable
- Verifiable
- Reproducible
- Auditable
Error Handling
The Filesystem Handler provides detailed error information for various failure scenarios:
- Permission Errors: When trying to access files outside the allowed path
- Not Found Errors: When a file or directory doesn't exist
- IO Errors: When read/write operations fail
- Format Errors: When paths are invalid
- Operation Not Allowed: When trying to use a disabled operation
All errors are returned as strings and are also recorded in the state chain.
Security Considerations
When using the Filesystem Handler, consider the following security aspects:
- Path Configuration: Set the base path to limit file access
- Limited Permissions: Use
allowed_commands
to restrict operations - Input Validation: Validate file paths before using them
- Content Validation: Validate file contents before writing
- Error Handling: Properly handle all error cases
- Resource Limits: Be mindful of file sizes and disk usage
Implementation Details
Under the hood, the Filesystem Handler:
- Validates all paths against the configured base directory
- Translates WIT interface calls to Rust's standard library file operations
- Handles errors and security checks
- Records all operations in the state chain
- Manages file handles and resources properly
Limitations
The current Filesystem Handler implementation has some limitations:
- No Streaming: Large files are loaded fully into memory
- No Symbolic Link Following: Symbolic links are not followed
- Limited Metadata: Some platform-specific file metadata is not available
- No File Locking: Concurrent access is not protected by file locks
- No Special Files: Device files, sockets, etc. are not supported
Best Practices
- Path Management: Use relative paths within your base directory
- Error Handling: Always handle file operation errors properly
- Resource Cleanup: Clean up temporary files when they're no longer needed
- Directory Structure: Create a clear directory structure for your actor's data
- Rate Limiting: Implement rate limiting for frequent file operations
- Backups: Implement backup mechanisms for important data
Examples
Example 1: Configuration Management
#![allow(unused)] fn main() { // Load configuration fn load_config() -> Result<Config, String> { match filesystem::exists("config.json") { Ok(exists) => { if exists { match filesystem::read_file("config.json") { Ok(content) => { let config: Config = serde_json::from_slice(&content) .map_err(|e| format!("Failed to parse config: {}", e))?; Ok(config) }, Err(e) => Err(format!("Failed to read config: {}", e)), } } else { // Create default config if not exists let default_config = Config::default(); save_config(&default_config)?; Ok(default_config) } }, Err(e) => Err(format!("Failed to check config existence: {}", e)), } } // Save configuration fn save_config(config: &Config) -> Result<(), String> { let content = serde_json::to_vec(config) .map_err(|e| format!("Failed to serialize config: {}", e))?; filesystem::write_file("config.json", content) .map_err(|e| format!("Failed to write config: {}", e)) } }
Example 2: Log Management
#![allow(unused)] fn main() { fn log_event(event: &Event) -> Result<(), String> { // Create logs directory if not exists match filesystem::exists("logs") { Ok(exists) => { if !exists { filesystem::create_dir("logs") .map_err(|e| format!("Failed to create logs directory: {}", e))?; } }, Err(e) => return Err(format!("Failed to check logs directory: {}", e)), } // Format log entry let timestamp = chrono::Utc::now().to_rfc3339(); let log_entry = format!("[{}] {}\n", timestamp, event.to_string()); // Append to log file filesystem::append_file("logs/events.log", log_entry.into_bytes()) .map_err(|e| format!("Failed to write log: {}", e)) } // Rotate logs if they get too large fn rotate_logs_if_needed() -> Result<(), String> { match filesystem::metadata("logs/events.log") { Ok(metadata) => { if metadata.size > MAX_LOG_SIZE { // Generate backup filename with timestamp let timestamp = chrono::Utc::now().timestamp(); let backup_name = format!("logs/events-{}.log", timestamp); // Read current log let content = filesystem::read_file("logs/events.log") .map_err(|e| format!("Failed to read log for rotation: {}", e))?; // Write to backup file filesystem::write_file(&backup_name, content) .map_err(|e| format!("Failed to write backup log: {}", e))?; // Clear original log filesystem::write_file("logs/events.log", vec![]) .map_err(|e| format!("Failed to clear log: {}", e))?; } Ok(()) }, Err(_) => Ok(()), // Log file doesn't exist yet, nothing to rotate } } }
Related Topics
- Store Handler - Alternative storage mechanism with content-addressable features
- HTTP Framework Handler - For creating HTTP endpoints that serve files
- Runtime Handler - For accessing runtime information and operations
Supervisor Handler
The Supervisor Handler enables parent-child relationships between actors in Theater. It provides the foundation for actor supervision hierarchies, allowing parent actors to spawn, monitor, and control child actors while maintaining the Theater security and verification model.
Overview
The Supervisor Handler implements the ntwk:theater/supervisor
interface, enabling actors to:
- Spawn new child actors
- Monitor child actor lifecycle events
- Access child actor state and events
- Stop and restart child actors
- Implement supervision strategies for fault tolerance
Configuration
To use the Supervisor Handler, add it to your actor's manifest:
[[handlers]]
type = "supervisor"
config = {}
The Supervisor Handler doesn't currently require any specific configuration parameters.
Interface
The Supervisor Handler is defined using the following WIT interface:
interface supervisor {
// Spawn a new child actor from a manifest
spawn: func(manifest: string) -> result<string, string>;
// List all child actor IDs
list-children: func() -> list<string>;
// Stop a specific child actor
stop-child: func(child-id: string) -> result<_, string>;
// Restart a specific child actor
restart-child: func(child-id: string) -> result<_, string>;
// Get the current state of a child actor
get-child-state: func(child-id: string) -> result<list<u8>, string>;
// Get the event history of a child actor
get-child-events: func(child-id: string) -> result<list<chain-event>, string>;
// Record structure for chain events
record chain-event {
hash: list<u8>,
parent-hash: option<list<u8>>,
event-type: string,
data: list<u8>,
timestamp: u64
}
}
Spawning Child Actors
The most fundamental operation in the supervision system is spawning child actors. This is done using the spawn
function:
#![allow(unused)] fn main() { // Manifest can be a path to a TOML file or the TOML content as a string let manifest = r#" name = "child-actor" component_path = "child_actor.wasm" [[handlers]] type = "message-server" config = {} "#; match supervisor::spawn(manifest) { Ok(child_id) => { println!("Spawned child actor with ID: {}", child_id); // Store the child ID for future reference }, Err(error) => { println!("Failed to spawn child actor: {}", error); } } }
Managing Child Actors
Listing Children
To get a list of all child actors:
#![allow(unused)] fn main() { let children = supervisor::list_children(); println!("Child actors: {:?}", children); }
Stopping a Child
To gracefully stop a child actor:
#![allow(unused)] fn main() { match supervisor::stop_child(child_id) { Ok(_) => { println!("Child actor stopped successfully"); }, Err(error) => { println!("Failed to stop child actor: {}", error); } } }
Restarting a Child
To restart a child actor (useful after failures):
#![allow(unused)] fn main() { match supervisor::restart_child(child_id) { Ok(_) => { println!("Child actor restarted successfully"); }, Err(error) => { println!("Failed to restart child actor: {}", error); } } }
Accessing Child State and Events
One of the powerful features of the supervision system is the ability to access child actor state and event history.
Getting Child State
To get the current state of a child actor:
#![allow(unused)] fn main() { match supervisor::get_child_state(child_id) { Ok(state_bytes) => { // Process the child state if let Some(bytes) = state_bytes { let state: ChildState = serde_json::from_slice(&bytes) .expect("Failed to deserialize child state"); println!("Child state: {:?}", state); } else { println!("Child has no state"); } }, Err(error) => { println!("Failed to get child state: {}", error); } } }
Getting Child Events
To get the event history of a child actor:
#![allow(unused)] fn main() { match supervisor::get_child_events(child_id) { Ok(events) => { println!("Child has {} events", events.len()); for event in events { println!("Event type: {}", event.event_type); println!("Timestamp: {}", event.timestamp); // Process event data based on type // ... } }, Err(error) => { println!("Failed to get child events: {}", error); } } }
Supervision Strategies
The Supervisor Handler enables the implementation of different supervision strategies inspired by the Erlang/OTP model:
One-for-One Strategy
Restart only the failed child:
#![allow(unused)] fn main() { fn handle_child_failure(child_id: &str) -> Result<(), String> { // Attempt to restart the failed child supervisor::restart_child(child_id) } }
All-for-One Strategy
Restart all children when one fails:
#![allow(unused)] fn main() { fn handle_child_failure(failed_child_id: &str) -> Result<(), String> { // Get all children let children = supervisor::list_children(); // Restart all children for child_id in children { supervisor::restart_child(&child_id)?; } Ok(()) } }
Rest-for-One Strategy
Restart the failed child and all children that depend on it:
#![allow(unused)] fn main() { fn handle_child_failure(failed_child_id: &str) -> Result<(), String> { // Get dependency tree (implementation specific) let dependent_children = get_dependent_children(failed_child_id); // Restart the failed child first supervisor::restart_child(failed_child_id)?; // Then restart dependent children for child_id in dependent_children { supervisor::restart_child(&child_id)?; } Ok(()) } }
State Chain Integration
All supervision operations are recorded in the parent actor's state chain, creating a verifiable history. The chain events include:
-
SupervisorOperation: Records details of supervision operations:
- Operation type (spawn, stop, restart, etc.)
- Child actor ID
- Result (success/failure)
-
ChildLifecycleEvent: Records child lifecycle events:
- Child actor ID
- Event type (started, stopped, crashed, etc.)
- Timestamp
This integration ensures that all supervision activities are:
- Traceable
- Verifiable
- Reproducible
- Auditable
Error Handling
The Supervisor Handler provides detailed error information for various failure scenarios:
- Spawn Errors: When child actor creation fails
- Stop Errors: When child actor termination fails
- Restart Errors: When child actor restart fails
- Not Found Errors: When the specified child actor doesn't exist
- Access Errors: When accessing child state or events fails
Security Considerations
When using the Supervisor Handler, consider the following security aspects:
- Child Isolation: Child actors run in separate WebAssembly sandboxes
- State Access Controls: Only direct parent actors can access child state
- Manifest Validation: Validate manifests before spawning actors
- Resource Limits: Consider setting limits on child actor resource usage
- Privilege Separation: Design actor hierarchies with security in mind
Implementation Details
Under the hood, the Supervisor Handler:
- Communicates with the Theater runtime to manage child actors
- Tracks parent-child relationships in the actor system
- Routes supervision commands to the appropriate actors
- Manages actor processes and mailboxes
- Handles actor lifecycle events
- Records all supervision activities in the state chain
Building Supervision Trees
Supervision trees are a powerful pattern for structuring actor systems. Here's how to build a basic supervision tree:
#![allow(unused)] fn main() { // Spawn root supervisor actor fn init() -> Result<(), String> { // Spawn worker actors let worker1_id = spawn_worker("worker1")?; let worker2_id = spawn_worker("worker2")?; // Spawn supervisor for a group of related workers let group_supervisor_id = spawn_group_supervisor()?; // Store child IDs for future reference let mut state = get_current_state(); state.children = vec![worker1_id, worker2_id, group_supervisor_id]; update_state(state); Ok(()) } // Function to spawn a worker actor fn spawn_worker(name: &str) -> Result<String, String> { let manifest = format!(r#" name = "{}" component_path = "worker.wasm" [[handlers]] type = "message-server" config = {{}} "#, name); supervisor::spawn(&manifest) } // Function to spawn a group supervisor fn spawn_group_supervisor() -> Result<String, String> { let manifest = r#" name = "group-supervisor" component_path = "supervisor.wasm" [[handlers]] type = "supervisor" config = {} [[handlers]] type = "message-server" config = {} "#; let supervisor_id = supervisor::spawn(manifest)?; // Send message to initialize the group supervisor // This will cause it to spawn its own child workers message_server::request(supervisor_id, init_message())?; Ok(supervisor_id) } }
Best Practices
- Hierarchical Design: Design clear supervision hierarchies
- Failure Domains: Group related actors under the same supervisor
- Restart Strategies: Choose appropriate restart strategies for different components
- State Recovery: Design child actors to recover gracefully from restarts
- Error Handling: Handle supervision errors properly
- Monitoring: Implement monitoring for supervisor decisions
- Testing: Test supervisor behavior with fault injection
Dynamic Supervision
You can implement dynamic supervision patterns where actors are spawned and managed at runtime:
#![allow(unused)] fn main() { // Handle a request to create a new worker fn handle_create_worker_request(params: CreateWorkerParams) -> Result<WorkerCreatedResponse, String> { // Create a manifest dynamically based on parameters let manifest = format!(r#" name = "{}" component_path = "{}" [[handlers]] type = "message-server" config = {{}} Additional handlers based on parameters {} "#, params.name, params.component_path, generate_handler_config(¶ms)); // Spawn the worker let worker_id = supervisor::spawn(&manifest)?; // Update supervisor state with new worker let mut current_state = get_current_state(); current_state.workers.push(WorkerInfo { id: worker_id.clone(), name: params.name.clone(), created_at: get_current_time(), }); update_state(current_state); // Return worker ID to requester Ok(WorkerCreatedResponse { worker_id, status: "created".to_string(), }) } }
Related Topics
- Message Server Handler - For actor-to-actor communication
- Runtime Handler - For accessing runtime information and operations
- Store Handler - For content-addressable storage
- State Management - For understanding state chain integration
- Supervision - For deeper supervision concepts
Store Handler
The Store Handler provides actors with access to Theater's content-addressable storage system. It enables actors to store and retrieve data using content hashes, create and manage labels for easier reference, and maintain persistent data across actor restarts.
Overview
The Store Handler implements the ntwk:theater/store
interface, enabling actors to:
- Create and manage store instances
- Store and retrieve data using content-addressable storage
- Create and manage labels for easy content reference
- Check for content existence and calculate storage size
- Efficiently deduplicate content
- Persistently store data across actor restarts and system reboots
Configuration
To use the Store Handler, add it to your actor's manifest:
[[handlers]]
type = "store"
config = {}
The Store Handler doesn't currently require any specific configuration parameters.
Interface
The Store Handler is defined using the following WIT interface:
interface store {
/// A reference to content in the store
record content-ref {
hash: string,
}
/// Create a new store
new: func() -> result<string, string>;
/// Store content and return a reference
store: func(store-id: string, content: list<u8>) -> result<content-ref, string>;
/// Retrieve content by reference
get: func(store-id: string, content-ref: content-ref) -> result<list<u8>, string>;
/// Check if content exists
exists: func(store-id: string, content-ref: content-ref) -> result<bool, string>;
/// Label content with a string identifier
label: func(store-id: string, label: string, content-ref: content-ref) -> result<_, string>;
/// Get content reference by label (returns None if label doesn't exist)
get-by-label: func(store-id: string, label: string) -> result<option<content-ref>, string>;
/// Store content and label it in one operation
store-at-label: func(store-id: string, label: string, content: list<u8>) -> result<content-ref, string>;
/// Replace content at a label
replace-content-at-label: func(store-id: string, label: string, content: list<u8>) -> result<content-ref, string>;
/// Replace a content reference at a label
replace-at-label: func(store-id: string, label: string, content-ref: content-ref) -> result<_, string>;
/// Remove a label
remove-label: func(store-id: string, label: string) -> result<_, string>;
/// List all labels in the store
list-labels: func(store-id: string) -> result<list<string>, string>;
/// List all content in the store
list-all-content: func(store-id: string) -> result<list<content-ref>, string>;
/// Calculate total size of all content in the store
calculate-total-size: func(store-id: string) -> result<u64, string>;
}
Store Management Operations
Creating a Store
To create a new store instance:
#![allow(unused)] fn main() { match store::new() { Ok(store_id) => { println!("Created new store with ID: {}", store_id); // Save the store ID for future operations }, Err(error) => { println!("Failed to create store: {}", error); } } }
Content Storage Operations
Storing Content
To store content in the store:
#![allow(unused)] fn main() { let data = b"Important data".to_vec(); match store::store(store_id.clone(), data) { Ok(content_ref) => { println!("Content stored with hash: {}", content_ref.hash); // Save the content reference for future use }, Err(error) => { println!("Failed to store content: {}", error); } } }
Retrieving Content
To retrieve content using a content reference:
#![allow(unused)] fn main() { match store::get(store_id.clone(), content_ref) { Ok(content) => { // Process the retrieved content let text = String::from_utf8(content).expect("Not valid UTF-8"); println!("Retrieved content: {}", text); }, Err(error) => { println!("Failed to retrieve content: {}", error); } } }
Checking Content Existence
To check if content exists in the store:
#![allow(unused)] fn main() { match store::exists(store_id.clone(), content_ref) { Ok(exists) => { if exists { println!("Content exists in the store"); } else { println!("Content does not exist in the store"); } }, Err(error) => { println!("Failed to check content existence: {}", error); } } }
Label Operations
Labels provide a way to assign human-readable names to content references, making it easier to retrieve them later.
Creating Labels
To create a label for content:
#![allow(unused)] fn main() { match store::label(store_id.clone(), "important-data", content_ref) { Ok(_) => { println!("Label 'important-data' created successfully"); }, Err(error) => { println!("Failed to create label: {}", error); } } }
Getting Content by Label
To retrieve content reference using a label:
#![allow(unused)] fn main() { match store::get_by_label(store_id.clone(), "important-data") { Ok(content_ref_opt) => { if let Some(content_ref) = content_ref_opt { // Use the content reference to get the actual content let content = store::get(store_id.clone(), content_ref)?; println!("Retrieved content for label 'important-data'"); } else { println!("Label 'important-data' does not exist"); } }, Err(error) => { println!("Failed to get content by label: {}", error); } } }
Storing and Labeling in One Operation
To store content and create a label in one operation:
#![allow(unused)] fn main() { let data = b"New data".to_vec(); match store::store_at_label(store_id.clone(), "new-data", data) { Ok(content_ref) => { println!("Content stored and labeled as 'new-data'"); }, Err(error) => { println!("Failed to store and label content: {}", error); } } }
Replacing Content at a Label
To replace the content referenced by a label:
#![allow(unused)] fn main() { let updated_data = b"Updated data".to_vec(); match store::replace_content_at_label(store_id.clone(), "new-data", updated_data) { Ok(content_ref) => { println!("Content at label 'new-data' updated successfully"); }, Err(error) => { println!("Failed to update content: {}", error); } } }
Replacing a Content Reference at a Label
To replace the content reference at a label with another existing reference:
#![allow(unused)] fn main() { match store::replace_at_label(store_id.clone(), "new-data", existing_content_ref) { Ok(_) => { println!("Content reference at label 'new-data' replaced successfully"); }, Err(error) => { println!("Failed to replace content reference: {}", error); } } }
Listing Labels
To get a list of all labels:
#![allow(unused)] fn main() { match store::list_labels(store_id.clone()) { Ok(labels) => { println!("Available labels:"); for label in labels { println!("- {}", label); } }, Err(error) => { println!("Failed to list labels: {}", error); } } }
Removing Labels
To remove a label:
#![allow(unused)] fn main() { match store::remove_label(store_id.clone(), "temporary-data") { Ok(_) => { println!("Label 'temporary-data' removed successfully"); }, Err(error) => { println!("Failed to remove label: {}", error); } } }
Store Management
Calculating Total Size
To calculate the total size of all stored content:
#![allow(unused)] fn main() { match store::calculate_total_size(store_id.clone()) { Ok(size) => { println!("Total storage size: {} bytes", size); }, Err(error) => { println!("Failed to calculate storage size: {}", error); } } }
Listing All Content
To list all content references in the store:
#![allow(unused)] fn main() { match store::list_all_content(store_id.clone()) { Ok(refs) => { println!("Total content items: {}", refs.len()); for content_ref in refs { println!("- {}", content_ref.hash); } }, Err(error) => { println!("Failed to list content: {}", error); } } }
Label Naming Conventions
While you can use any string as a label, it's good practice to follow certain conventions:
-
Actor-Specific Labels: Prefix labels with the actor ID or name
actor:12345:config
-
Versioned Labels: Include version information in labels
config:v1.0
-
Type Labels: Include content type in the label
image:logo
-
Namespaced Labels: Use namespaces for organization
app:settings:theme
State Chain Integration
Store operations are recorded in the actor's state chain, ensuring a verifiable history of all storage interactions. The chain events include:
Call Events
NewStoreCall
StoreCall
GetCall
ExistsCall
LabelCall
GetByLabelCall
StoreAtLabelCall
ReplaceContentAtLabelCall
ReplaceAtLabelCall
RemoveLabelCall
ListLabelsCall
ListAllContentCall
CalculateTotalSizeCall
Result Events
NewStoreResult
StoreResult
GetResult
ExistsResult
LabelResult
GetByLabelResult
StoreAtLabelResult
ReplaceContentAtLabelResult
ReplaceAtLabelResult
RemoveLabelResult
ListLabelsResult
ListAllContentResult
CalculateTotalSizeResult
Error Events
Error
(includes operation type and error message)
Each event includes detailed information such as store ID, content references, labels, and success/failure status.
Error Handling
The Store Handler provides detailed error information for various failure scenarios:
- Storage Errors: When content storage fails
- Retrieval Errors: When content retrieval fails
- Label Errors: When label operations fail
- Not Found Errors: When content or labels don't exist
- IO Errors: When disk operations fail
Security Considerations
When using the Store Handler, consider the following security aspects:
- Content Validation: Validate data before storing it
- Label Namespaces: Use namespaced labels to avoid conflicts
- Size Limits: Be mindful of storage size and implement limits
- Sensitive Data: Consider encrypting sensitive data before storage
- Cleanup: Implement policies for removing unused content
Implementation Details
Under the hood, the Store Handler:
- Uses SHA-1 hashing to create unique content identifiers
- Stores content in a directory structure organized by store ID
- Maintains separate directories for content and label mappings
- Records detailed events for all operations
- Ensures data integrity through content verification
Storage Structure
The physical storage is organized as follows:
store/
├── <store-uuid1>/ # Store instance 1
│ ├── data/ # Content files stored by hash
│ │ ├── <hash1>
│ │ ├── <hash2>
│ │ └── ...
│ └── labels/ # Labels pointing to content hashes
│ ├── <label1>
│ ├── <label2>
│ └── ...
├── <store-uuid2>/ # Store instance 2
...
└── manifest/ # System metadata
Best Practices
- Store Management: Create separate stores for different use cases
- Content Size: The store is optimized for small to medium content sizes (< 10MB)
- Reference Tracking: Keep track of content references for important data
- Label Schemes: Develop consistent label naming schemes
- Cleanup: Implement periodic cleanup for unused content
- Error Handling: Always handle store operation errors appropriately
- Caching: Consider implementing local caching for frequently accessed content
Common Use Cases
Configuration Storage
#![allow(unused)] fn main() { // Store configuration fn save_config(store_id: &str, config: &Config) -> Result<(), String> { let config_bytes = serde_json::to_vec(config) .map_err(|e| format!("Failed to serialize config: {}", e))?; store::store_at_label(store_id.to_string(), "app:config", config_bytes) .map(|_| ()) .map_err(|e| format!("Failed to store config: {}", e)) } // Load configuration fn load_config(store_id: &str) -> Result<Config, String> { let content_ref_opt = store::get_by_label(store_id.to_string(), "app:config") .map_err(|e| format!("Failed to get config reference: {}", e))?; if let Some(content_ref) = content_ref_opt { let config_bytes = store::get(store_id.to_string(), content_ref) .map_err(|e| format!("Failed to retrieve config: {}", e))?; let config: Config = serde_json::from_slice(&config_bytes) .map_err(|e| format!("Failed to deserialize config: {}", e))?; Ok(config) } else { Err("Configuration not found".to_string()) } } }
Content Deduplication
#![allow(unused)] fn main() { fn store_with_deduplication(store_id: &str, data: Vec<u8>) -> Result<ContentRef, String> { // Generate a hash to check if the content already exists use sha1::{Sha1, Digest}; let mut hasher = Sha1::new(); hasher.update(&data); let hash = format!("{:x}", hasher.finalize()); // Create a content reference to check existence let content_ref = ContentRef { hash }; // Check if the content already exists if store::exists(store_id.to_string(), content_ref.clone())? { println!("Content already exists in store, reusing existing reference"); return Ok(content_ref); } // Content doesn't exist, store it store::store(store_id.to_string(), data) } }
Versioned Content
#![allow(unused)] fn main() { fn store_versioned_content(store_id: &str, name: &str, version: &str, data: Vec<u8>) -> Result<(), String> { // Store the content let content_ref = store::store(store_id.to_string(), data)?; // Create a versioned label let versioned_label = format!("{}:v{}", name, version); store::label(store_id.to_string(), versioned_label, content_ref.clone())?; // Always update the 'latest' label let latest_label = format!("{}:latest", name); store::label(store_id.to_string(), latest_label, content_ref)?; Ok(()) } fn get_content_version(store_id: &str, name: &str, version: &str) -> Result<Vec<u8>, String> { let label = format!("{}:v{}", name, version); let content_ref_opt = store::get_by_label(store_id.to_string(), label)?; if let Some(content_ref) = content_ref_opt { store::get(store_id.to_string(), content_ref) } else { Err(format!("Version {} not found", version)) } } fn get_latest_content(store_id: &str, name: &str) -> Result<Vec<u8>, String> { let label = format!("{}:latest", name); let content_ref_opt = store::get_by_label(store_id.to_string(), label)?; if let Some(content_ref) = content_ref_opt { store::get(store_id.to_string(), content_ref) } else { Err(format!("No versions available for {}", name)) } } }
Related Topics
- Filesystem Handler - Alternative file access mechanism
- State Management - For understanding state chain integration
- Store System - For deeper store concepts
- Store API for Actors - For actor-specific store usage
- Store Usage Patterns - For common usage patterns and examples
Event Types Reference
The Store Handler tracks detailed events for all operations. Here's a complete reference of the event types:
Call Events
Event Type | Description | Parameters |
---|---|---|
NewStoreCall | Called when creating a new store | None |
StoreCall | Called when storing content | store_id , content |
GetCall | Called when retrieving content | store_id , content_ref |
ExistsCall | Called when checking if content exists | store_id , content_ref |
LabelCall | Called when labeling content | store_id , label , content_ref |
GetByLabelCall | Called when getting content by label | store_id , label |
StoreAtLabelCall | Called when storing and labeling content | store_id , label , content |
ReplaceContentAtLabelCall | Called when replacing content at a label | store_id , label , content |
ReplaceAtLabelCall | Called when replacing a reference at a label | store_id , label , content_ref |
RemoveLabelCall | Called when removing a label | store_id , label |
ListLabelsCall | Called when listing all labels | store_id |
ListAllContentCall | Called when listing all content | store_id |
CalculateTotalSizeCall | Called when calculating total size | store_id |
Result Events
Event Type | Description | Parameters |
---|---|---|
NewStoreResult | Result of creating a new store | store_id , success |
StoreResult | Result of storing content | store_id , content_ref , success |
GetResult | Result of retrieving content | store_id , content_ref , content , success |
ExistsResult | Result of checking if content exists | store_id , content_ref , exists , success |
LabelResult | Result of labeling content | store_id , label , content_ref , success |
GetByLabelResult | Result of getting content by label | store_id , label , content_ref , success |
StoreAtLabelResult | Result of storing and labeling content | store_id , label , content_ref , success |
ReplaceContentAtLabelResult | Result of replacing content at a label | store_id , label , content_ref , success |
ReplaceAtLabelResult | Result of replacing a reference at a label | store_id , label , content_ref , success |
RemoveLabelResult | Result of removing a label | store_id , label , success |
ListLabelsResult | Result of listing all labels | store_id , labels , success |
ListAllContentResult | Result of listing all content | store_id , content_refs , success |
CalculateTotalSizeResult | Result of calculating total size | store_id , size , success |
Error Events
Event Type | Description | Parameters |
---|---|---|
Error | Records an error with any operation | operation , message |
Each event includes a timestamp and optional description field in addition to the operation-specific parameters.
Runtime Handler
The Runtime Handler provides actors with information about and control over their runtime environment in Theater. It enables actors to access runtime metadata, manage their lifecycle, and interact with the Theater runtime system.
Overview
The Runtime Handler implements the ntwk:theater/runtime
interface, providing actors with the ability to:
- Access information about themselves and the runtime
- Control their lifecycle
- Get system and environment information
- Record custom metrics and events
- Manage runtime resources
Configuration
To use the Runtime Handler, add it to your actor's manifest:
[[handlers]]
type = "runtime"
config = {}
The Runtime Handler doesn't currently require any specific configuration parameters.
Interface
The Runtime Handler is defined using the following WIT interface:
interface runtime {
// Get the actor's unique ID
get-actor-id: func() -> string;
// Get the actor's name
get-actor-name: func() -> string;
// Get current timestamp (milliseconds since epoch)
get-current-time: func() -> u64;
// Get environment variable value
get-env: func(name: string) -> option<string>;
// Log a message with specified level
log: func(level: string, message: string) -> result<_, string>;
// Record a custom metric
record-metric: func(name: string, value: float64) -> result<_, string>;
// Record a custom event
record-event: func(event-type: string, data: list<u8>) -> result<_, string>;
// Get theater version
get-theater-version: func() -> string;
// Get system information
get-system-info: func() -> system-info;
// Runtime statistics and information
record system-info {
hostname: string,
os-type: string,
os-release: string,
cpu-count: u32,
memory-total: u64,
memory-available: u64,
uptime: u64,
}
}
Runtime Information
Getting Actor Information
To get the actor's ID and name:
#![allow(unused)] fn main() { let actor_id = runtime::get_actor_id(); let actor_name = runtime::get_actor_name(); println!("Actor ID: {}", actor_id); println!("Actor name: {}", actor_name); }
Getting Current Time
To get the current time (milliseconds since epoch):
#![allow(unused)] fn main() { let now = runtime::get_current_time(); println!("Current time: {} ms", now); }
Getting Theater Version
To get the current Theater runtime version:
#![allow(unused)] fn main() { let version = runtime::get_theater_version(); println!("Theater version: {}", version); }
Getting System Information
To get information about the system:
#![allow(unused)] fn main() { let system_info = runtime::get_system_info(); println!("System Information:"); println!("Hostname: {}", system_info.hostname); println!("OS Type: {}", system_info.os_type); println!("OS Release: {}", system_info.os_release); println!("CPU Count: {}", system_info.cpu_count); println!("Total Memory: {} bytes", system_info.memory_total); println!("Available Memory: {} bytes", system_info.memory_available); println!("System Uptime: {} seconds", system_info.uptime); }
Getting Environment Variables
To access environment variables:
#![allow(unused)] fn main() { if let Some(log_level) = runtime::get_env("LOG_LEVEL") { println!("Log level from environment: {}", log_level); } else { println!("LOG_LEVEL environment variable not set"); } }
Logging and Events
Logging Messages
To log messages at different levels:
#![allow(unused)] fn main() { // Log with different levels runtime::log("debug", "This is a debug message").unwrap(); runtime::log("info", "This is an info message").unwrap(); runtime::log("warn", "This is a warning message").unwrap(); runtime::log("error", "This is an error message").unwrap(); }
Recording Custom Metrics
To record custom metrics:
#![allow(unused)] fn main() { // Record a performance metric runtime::record_metric("request_duration_ms", 42.5).unwrap(); // Record a counter runtime::record_metric("requests_processed", 1.0).unwrap(); // Record memory usage runtime::record_metric("memory_usage_bytes", 1024.0 * 1024.0).unwrap(); }
Recording Custom Events
To record custom events:
#![allow(unused)] fn main() { // Record a simple event let event_data = b"User logged in".to_vec(); runtime::record_event("user_login", event_data).unwrap(); // Record a structured event let complex_event = serde_json::json!({ "action": "item_purchase", "user_id": "user123", "item_id": "item456", "amount": 29.99, "currency": "USD" }); let event_bytes = serde_json::to_vec(&complex_event).unwrap(); runtime::record_event("purchase", event_bytes).unwrap(); }
State Chain Integration
All runtime operations are recorded in the actor's state chain, creating a verifiable history. The chain events include:
- RuntimeOperation: Records runtime operations like environment variable access or system info requests
- CustomEvent: Records user-defined events with their data
- LogEvent: Records log messages with their level
- MetricEvent: Records custom metrics with their values
Error Handling
The Runtime Handler provides error information for various failure scenarios:
- Log Errors: When logging fails
- Metric Errors: When metric recording fails
- Event Errors: When custom event recording fails
- Environment Errors: When environment variable access fails
Security Considerations
When using the Runtime Handler, consider the following security aspects:
- Environment Variables: Be careful with sensitive environment variables
- Logging: Don't log sensitive data like passwords or tokens
- Metrics: Avoid using personally identifiable information in metric names
- Custom Events: Be mindful of the data included in custom events
- System Information: Consider what system information is exposed to actors
Implementation Details
Under the hood, the Runtime Handler:
- Provides a bridge between WebAssembly actors and the host runtime
- Translates WIT interface calls to host runtime operations
- Records all operations in the state chain
- Manages access to system resources and information
- Interacts with the logging and metrics subsystems
Use Cases
Application Monitoring
#![allow(unused)] fn main() { // Record application health metrics periodically fn record_health_metrics() -> Result<(), String> { // Get system information let system_info = runtime::get_system_info(); // Record memory metrics let memory_used = system_info.memory_total - system_info.memory_available; runtime::record_metric("memory_used_bytes", memory_used as f64)?; // Record memory percentage let memory_percentage = (memory_used as f64 / system_info.memory_total as f64) * 100.0; runtime::record_metric("memory_usage_percent", memory_percentage)?; // Record CPU metrics (application-specific) let cpu_usage = calculate_cpu_usage(); runtime::record_metric("cpu_usage_percent", cpu_usage)?; // Log status runtime::log("info", &format!("Health metrics recorded: Memory {}%, CPU {}%", memory_percentage, cpu_usage))?; Ok(()) } }
Structured Logging
#![allow(unused)] fn main() { // Structured logging helper fn log_structured(level: &str, message: &str, context: &serde_json::Value) -> Result<(), String> { let log_entry = serde_json::json!({ "message": message, "timestamp": runtime::get_current_time(), "actor": { "id": runtime::get_actor_id(), "name": runtime::get_actor_name(), }, "context": context }); let log_message = serde_json::to_string(&log_entry) .map_err(|e| format!("Failed to serialize log: {}", e))?; runtime::log(level, &log_message) } // Usage fn process_request(request: &Request) -> Result<Response, String> { // Log request received log_structured("info", "Request received", &serde_json::json!({ "request_id": request.id, "client_ip": request.client_ip, "method": request.method, "path": request.path }))?; // Process request let start_time = runtime::get_current_time(); let result = handle_request(request); let duration = runtime::get_current_time() - start_time; // Record processing time runtime::record_metric("request_duration_ms", duration as f64)?; // Log result match &result { Ok(response) => { log_structured("info", "Request completed", &serde_json::json!({ "request_id": request.id, "status": response.status, "duration_ms": duration }))?; }, Err(error) => { log_structured("error", "Request failed", &serde_json::json!({ "request_id": request.id, "error": error, "duration_ms": duration }))?; } } result } }
Feature Flags
#![allow(unused)] fn main() { // Check if a feature is enabled via environment variables fn is_feature_enabled(feature_name: &str) -> bool { let env_var_name = format!("FEATURE_{}", feature_name.to_uppercase()); match runtime::get_env(&env_var_name) { Some(value) => { match value.to_lowercase().as_str() { "true" | "yes" | "1" => true, _ => false, } }, None => false, } } // Usage fn process_request(request: &Request) -> Response { if is_feature_enabled("new_ui") { // Use new UI processing process_with_new_ui(request) } else { // Use old UI processing process_with_old_ui(request) } } }
Best Practices
- Consistent Logging: Use consistent log levels and formats
- Meaningful Metrics: Design metrics that provide actionable insights
- Error Handling: Always handle errors from runtime functions
- Resource Usage: Be mindful of resource usage in metrics collection
- Security: Never log sensitive information
Performance Considerations
- Logging Overhead: Excessive logging can impact performance
- Metric Cardinality: Too many unique metric names can cause issues
- Event Size: Large event payloads may impact performance
- System Info Calls: Frequent system info calls may have overhead
Related Topics
- Message Server Handler - For actor-to-actor communication
- Supervisor Handler - For parent-child actor relationships
- Timing Handler - For timing and scheduling operations
- State Management - For state chain integration
Timing Handler
The Timing Handler provides actors with time-related capabilities, including delays, periodic scheduling, and timeout management. It enables actors to control the timing of their operations while maintaining Theater's state verification model.
Overview
The Timing Handler implements the ntwk:theater/timing
interface, enabling actors to:
- Introduce controlled delays in their execution
- Implement timeout patterns for operations
- Enforce rate limits and throttling
- Create periodic tasks and scheduled operations
Configuration
To use the Timing Handler, add it to your actor's manifest:
[[handlers]]
type = "timing"
config = {
max_sleep_duration = 3600000, # Maximum sleep duration in milliseconds (1 hour)
min_sleep_duration = 1 # Minimum sleep duration in milliseconds
}
Configuration options:
max_sleep_duration
: (Optional) Maximum allowed sleep duration in milliseconds, defaults to 3600000 (1 hour)min_sleep_duration
: (Optional) Minimum allowed sleep duration in milliseconds, defaults to 1
Interface
The Timing Handler is defined using the following WIT interface:
interface timing {
// Sleep for the specified duration (in milliseconds)
sleep: func(duration-ms: u64) -> result<_, string>;
// Get current timestamp (milliseconds since epoch)
now: func() -> u64;
// Get high-resolution time for performance measurement (in nanoseconds)
performance-now: func() -> u64;
}
Basic Timing Operations
Sleep
The sleep
function pauses actor execution for a specified duration:
#![allow(unused)] fn main() { // Sleep for 1 second match timing::sleep(1000) { Ok(_) => { println!("Resumed after 1 second"); }, Err(error) => { println!("Sleep operation failed: {}", error); } } }
Note that the sleep duration must fall within the configured min_sleep_duration
and max_sleep_duration
range. Attempting to sleep for longer than the maximum or shorter than the minimum will result in an error.
Current Time
The now
function returns the current time in milliseconds since the Unix epoch:
#![allow(unused)] fn main() { let current_time = timing::now(); println!("Current time: {} ms", current_time); }
Performance Timing
The performance-now
function provides high-resolution timing for performance measurement:
#![allow(unused)] fn main() { // Measure operation duration let start = timing::performance_now(); // Perform operation perform_expensive_operation(); let end = timing::performance_now(); let duration_ns = end - start; let duration_ms = duration_ns / 1_000_000; println!("Operation took {} ms", duration_ms); }
Common Patterns
Implementing Timeouts
#![allow(unused)] fn main() { // Perform an operation with a timeout fn perform_with_timeout<F, T>(operation: F, timeout_ms: u64) -> Result<T, String> where F: FnOnce() -> Result<T, String>, { // Create a oneshot channel for the result let (tx, rx) = tokio::sync::oneshot::channel(); // Spawn a task to perform the operation tokio::spawn(async move { match operation() { Ok(result) => { let _ = tx.send(Ok(result)); }, Err(err) => { let _ = tx.send(Err(err)); } } }); // Wait for the result or timeout match tokio::time::timeout(std::time::Duration::from_millis(timeout_ms), rx).await { Ok(result) => result.unwrap(), Err(_) => Err("Operation timed out".to_string()), } } // Usage let result = perform_with_timeout(|| { // Perform potentially long-running operation perform_api_call() }, 5000); // 5 second timeout }
Rate Limiting
#![allow(unused)] fn main() { // Simple rate limiter struct RateLimiter { last_operation: u64, min_interval_ms: u64, } impl RateLimiter { fn new(min_interval_ms: u64) -> Self { Self { last_operation: 0, min_interval_ms, } } fn check_and_update(&mut self) -> Result<(), String> { let now = timing::now(); let elapsed = now - self.last_operation; if self.last_operation == 0 || elapsed >= self.min_interval_ms { self.last_operation = now; Ok(()) } else { let wait_time = self.min_interval_ms - elapsed; timing::sleep(wait_time)?; self.last_operation = timing::now(); Ok(()) } } } // Usage let mut rate_limiter = RateLimiter::new(100); // 100ms between operations for item in items { // Ensure we don't exceed rate limit rate_limiter.check_and_update()?; // Process item process_item(item)?; } }
Periodic Tasks
#![allow(unused)] fn main() { // Run a task periodically fn run_periodically<F>(task: F, interval_ms: u64, max_iterations: Option<usize>) -> Result<(), String> where F: Fn() -> Result<(), String>, { let mut iterations = 0; loop { // Run the task task()?; // Check if we've reached the maximum iterations if let Some(max) = max_iterations { iterations += 1; if iterations >= max { break; } } // Sleep until the next interval timing::sleep(interval_ms)?; } Ok(()) } // Usage run_periodically(|| { // Periodic task logic collect_metrics() }, 5000, Some(10))?; // Run every 5 seconds, 10 times }
State Chain Integration
All timing operations are recorded in the actor's state chain, creating a verifiable history. The chain events include:
-
TimingOperation: Records details of timing operations:
- Operation type (sleep, now, performance-now)
- Duration (for sleep operations)
- Timestamp
-
Error: Records any errors that occur:
- Operation type
- Error message
This integration ensures that all timing activities are:
- Traceable
- Verifiable
- Reproducible
- Auditable
Error Handling
The Timing Handler provides error information for various failure scenarios:
- Duration Errors: When sleep duration is outside allowed range
- Operation Errors: When timing operations fail
- Resource Errors: When system resources are unavailable
Security Considerations
When using the Timing Handler, consider the following security aspects:
- Sleep Limits: The configuration enforces limits on sleep durations
- Resource Consumption: Long or frequent sleeps may impact system resources
- Timing Attacks: Be aware of potential timing side-channel attacks
Implementation Details
Under the hood, the Timing Handler:
- Uses the Tokio runtime for asynchronous sleep operations
- Enforces configurable minimum and maximum sleep durations
- Records all operations in the state chain
- Provides consistent time sources across the actor system
Performance Considerations
- Sleep Overhead: There is a small overhead for each sleep operation
- Time Resolution: Time functions have platform-dependent resolution
- Resource Usage: Excessive sleep operations may impact system performance
Best Practices
- Error Handling: Always handle errors from timing functions
- Sleep Duration: Use reasonable sleep durations
- Batch Processing: Consider batching operations instead of sleeping between each
- Timeouts: Implement timeouts for operations that may not complete
- Rate Limiting: Use rate limiting for external API calls
Examples
Retry Logic
#![allow(unused)] fn main() { // Retry an operation with exponential backoff fn retry_with_backoff<F, T>( operation: F, initial_backoff_ms: u64, max_backoff_ms: u64, max_retries: usize ) -> Result<T, String> where F: Fn() -> Result<T, String>, { let mut backoff = initial_backoff_ms; let mut attempts = 0; loop { match operation() { Ok(result) => return Ok(result), Err(error) => { attempts += 1; if attempts >= max_retries { return Err(format!("Operation failed after {} attempts: {}", attempts, error)); } // Log the failure and retry plan println!("Attempt {} failed: {}. Retrying in {} ms", attempts, error, backoff); // Wait before next attempt timing::sleep(backoff)?; // Exponential backoff with jitter let jitter = (backoff as f64 * 0.1 * rand::random::<f64>()) as u64; backoff = std::cmp::min(backoff * 2 + jitter, max_backoff_ms); } } } } // Usage let result = retry_with_backoff( || external_api_call("https://api.example.com/data"), 100, // Initial backoff of 100ms 30000, // Maximum backoff of 30 seconds 5 // Maximum 5 retry attempts )?; }
Debouncing
#![allow(unused)] fn main() { // Debounce a function call struct Debouncer { last_call: u64, timeout_ms: u64, } impl Debouncer { fn new(timeout_ms: u64) -> Self { Self { last_call: 0, timeout_ms, } } fn should_call(&mut self) -> bool { let now = timing::now(); if now - self.last_call >= self.timeout_ms { self.last_call = now; true } else { false } } } // Usage let mut input_debouncer = Debouncer::new(500); // 500ms debounce fn process_input(input: &str) { if input_debouncer.should_call() { // Process the input println!("Processing input: {}", input); } else { // Skip processing this input println!("Debounced input: {}", input); } } }
Measuring Request Latency
#![allow(unused)] fn main() { // Measure and log request latency fn measure_request_latency<F, T>(operation_name: &str, operation: F) -> Result<T, String> where F: FnOnce() -> Result<T, String>, { // Get start time in high resolution let start = timing::performance_now(); // Perform the operation let result = operation()?; // Calculate duration let end = timing::performance_now(); let duration_ns = end - start; let duration_ms = duration_ns / 1_000_000; // Log the latency println!("{} took {} ms", operation_name, duration_ms); // Record metric runtime::record_metric(&format!("{}_latency_ms", operation_name), duration_ms as f64)?; // Return the result Ok(result) } // Usage let user_data = measure_request_latency("fetch_user_data", || { api_client::get_user_data(user_id) })?; }
Related Topics
- Runtime Handler - For runtime information and operations
- Message Server Handler - For actor-to-actor communication
- State Management - For state chain integration
Theater API Documentation
This page provides an overview of the Theater API. For detailed reference documentation, you can check out the auto-generated rustdoc API Reference.
Note: The rustdoc API Reference provides detailed information about all types, functions, and modules directly from the code annotations.
Core Concepts
Theater uses WebAssembly components to create isolated, deterministic actors that communicate through a message-passing interface. Each actor is a WebAssembly component that implements specific interfaces defined using the WebAssembly Interface Type (WIT) system.
Key API Components
Here are some key components in the API:
- ActorRuntime - Manages the lifecycle of an actor
- ActorExecutor - Executes actor code in WebAssembly
- StateChain - Maintains the verifiable chain of state changes
- TheaterId - Unique identifier for actors
- ContentStore - Content-addressable storage system
Core Actor Interface
Every Theater actor must implement the core actor interface:
// ntwk:theater/actor interface
package ntwk:theater
interface types {
/// JSON-encoded data
type json = list<u8>
/// Event structure for actor messages
record event {
event-type: string,
parent: option<u64>,
data: json
}
}
interface actor {
use types.{json, event}
/// Initialize actor state
init: func() -> json
/// Handle an incoming event, returning new state
handle: func(evt: event, state: json) -> json
}
Implementation Example
Here's how to implement the core actor interface in Rust:
#![allow(unused)] fn main() { use bindings::exports::ntwk::theater::actor::Guest as ActorGuest; use bindings::ntwk::theater::types::{Event, Json}; use serde::{Deserialize, Serialize}; // Define your actor's state #[derive(Serialize, Deserialize)] struct State { count: i32, last_updated: String, } struct Component; impl ActorGuest for Component { // Initialize actor state fn init() -> Vec<u8> { let initial_state = State { count: 0, last_updated: chrono::Utc::now().to_string(), }; serde_json::to_vec(&initial_state).unwrap() } // Handle incoming messages fn handle(evt: Event, state: Vec<u8>) -> Vec<u8> { let mut current_state: State = serde_json::from_slice(&state).unwrap(); // Process the event if let Ok(message) = serde_json::from_slice(&evt.data) { // Update state based on message... } serde_json::to_vec(¤t_state).unwrap() } } bindings::export!(Component with_types_in bindings); }
Available Host Functions
Theater provides several host functions that actors can use. For complete details, see the host module documentation.
Runtime Interface
// ntwk:theater/runtime interface
interface runtime {
/// Log a message to the host system
log: func(msg: string)
/// Spawn a new actor from a manifest
spawn: func(manifest: string)
/// Get the current event chain
get-chain: func() -> chain
}
HTTP Server Interface
// ntwk:theater/http-server interface
interface http-server {
record http-request {
method: string,
path: string,
headers: list<tuple<string, string>>,
body: option<list<u8>>
}
record http-response {
status: u16,
headers: list<tuple<string, string>>,
body: option<list<u8>>
}
handle-request: func(req: http-request, state: json) -> tuple<http-response, json>
}
The HttpFramework provides the implementation of this interface.
WebSocket Server Interface
// ntwk:theater/websocket-server interface
interface websocket-server {
use types.{json}
/// Types of WebSocket messages
enum message-type {
text,
binary,
connect,
close,
ping,
pong,
other(string)
}
/// WebSocket message structure
record websocket-message {
ty: message-type,
data: option<list<u8>>,
text: option<string>
}
/// WebSocket response structure
record websocket-response {
messages: list<websocket-message>
}
/// Handle an incoming WebSocket message
handle-message: func(msg: websocket-message, state: json) -> tuple<json, websocket-response>
}
Handler Implementation Examples
HTTP Server Handler
#![allow(unused)] fn main() { use bindings::exports::ntwk::theater::http_server::Guest as HttpGuest; use bindings::ntwk::theater::types::Json; use bindings::ntwk::theater::http_server::{HttpRequest, HttpResponse}; impl HttpGuest for Component { fn handle_request(req: HttpRequest, state: Json) -> (HttpResponse, Json) { match (req.method.as_str(), req.path.as_str()) { ("GET", "/count") => { let current_state: State = serde_json::from_slice(&state).unwrap(); (HttpResponse { status: 200, headers: vec![ ("Content-Type".to_string(), "application/json".to_string()) ], body: Some(serde_json::json!({ "count": current_state.count }).to_string().into_bytes()), }, state) }, _ => (HttpResponse { status: 404, headers: vec![], body: None, }, state) } } } }
For more details on HTTP handling, see the HTTP Client documentation.
WebSocket Server Handler
#![allow(unused)] fn main() { use bindings::exports::ntwk::theater::websocket_server::Guest as WebSocketGuest; use bindings::ntwk::theater::types::Json; use bindings::ntwk::theater::websocket_server::{ WebSocketMessage, WebSocketResponse, MessageType }; impl WebSocketGuest for Component { fn handle_message(msg: WebSocketMessage, state: Json) -> (Json, WebSocketResponse) { let mut current_state: State = serde_json::from_slice(&state).unwrap(); let response = match msg.ty { MessageType::Text => { if let Some(text) = msg.text { // Process text message... WebSocketResponse { messages: vec![WebSocketMessage { ty: MessageType::Text, text: Some("Message received".to_string()), data: None, }] } } else { WebSocketResponse { messages: vec![] } } }, _ => WebSocketResponse { messages: vec![] } }; (serde_json::to_vec(¤t_state).unwrap(), response) } } }
Actor Configuration
Actors are configured using TOML manifests. See the ManifestConfig for details on the configuration options.
name = "example-actor"
component_path = "target/wasm32-wasi/release/example_actor.wasm"
[interface]
implements = "ntwk:theater/websocket-server"
requires = []
[[handlers]]
type = "websocket-server"
config = { port = 8080 }
[logging]
level = "debug"
Hash Chain Integration
Theater uses a hash chain to track state transitions. See the StateChain for more details.
Best Practices
-
State Management
- Use serde for state serialization
- Keep state JSON-serializable
- Include timestamps in state
- Handle serialization errors
-
Message Handling
- Validate message format
- Handle all message types
- Return consistent responses
- Preserve state on errors
-
Handler Implementation
- Implement appropriate interfaces
- Handle all request types
- Return proper responses
- Maintain state consistency
-
Error Handling
- Log errors with context
- Return unchanged state on error
- Validate all inputs
- Handle all error cases
Development Tips
- Use the chat-room example as a reference implementation
- Test with multiple handler types
- Monitor the hash chain during development
- Use logging for debugging
- Validate state transitions