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:

  1. WebAssembly Components & Sandboxing: Security boundaries and capability controls for agent containment
  2. Actor Model & Supervision: Isolated agents, message-passing communication, and fault tolerance through supervision
  3. 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.

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

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:

  1. WebAssembly Components & Sandboxing provide security boundaries and deterministic execution, ensuring that code operates within well-defined constraints.

  2. Actor Model & Supervision implements an Erlang-style actor system with hierarchical supervision, creating isolation between components and facilitating recovery from failures.

  3. 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:

  1. Security Boundaries: How do we ensure agents only access systems and data they're explicitly allowed to?
  2. Visibility and Transparency: How do we observe, audit, and understand what agents are doing?
  3. Coordination and Cooperation: How do we enable multiple specialized agents to work together safely?
  4. Failure Management: How do we handle agents that encounter errors or behave in unexpected ways?
  5. 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:

  1. 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.

  2. 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.

  3. 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:

  1. Strong Isolation: Preventing agents from interfering with each other or the host system.
  2. Precise Capability Control: Granting agents only the specific access rights they need.
  3. Execution Determinism: Enabling reliable replay, verification, and debugging of agent behavior.
  4. Implementation Flexibility: Allowing agents to be developed in various languages.
  5. Modularity and Composability: Facilitating the creation of complex agent systems from specialized components.
  6. 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:

  1. 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.
  2. Mailbox: Each agent has a mailbox where incoming messages are queued, enabling asynchronous communication.
  3. 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.
  4. 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:

  1. Clear Agent Boundaries: A natural model for defining the scope and capabilities of individual agents.
  2. Enhanced Fault Tolerance: The ability to contain failures and automatically recover parts of the agent system.
  3. Scalability: Agents can potentially be distributed across cores or machines to handle increased load.
  4. Resilience: Systems can remain partially or fully operational even when individual agents fail.
  5. 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:

  1. Transparency: Making agent behavior fully observable and understandable
  2. Powerful Debugging: Enabling precise reproduction and diagnosis of unexpected behaviors
  3. Auditability: Allowing independent verification of agent actions and decision processes
  4. Enhanced Trust: Providing strong evidence of agent behavior, critical for security and compliance
  5. 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 actor
  • component_path (required): Path to the WebAssembly component
  • description (optional): Human-readable description
  • version (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

  1. Configuration Organization

    • Keep configurations in dedicated directory
    • Use environment variables for secrets
    • Version control templates, not actual configs
    • Document all custom values
  2. Security

    • Never commit sensitive values
    • Use environment variables for credentials
    • Restrict file system access
    • Limit network access
  3. Development

    • Use separate dev configurations
    • Enable detailed logging
    • Configure mock services
    • Set reasonable resource limits
  4. Deployment

    • Use environment-specific configs
    • Validate all configurations
    • Monitor resource limits
    • Document all settings
  5. 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

CommandDescription
buildBuild a Theater actor to WebAssembly
createCreate a new Theater actor project
eventsGet actor events
inspectShow detailed information about an actor
listList all running actors
logsView actor logs
messageSend a message to an actor
restartRestart a running actor
serverStart a Theater server
shellStart an interactive shell
startStart an actor from a manifest
stateGet actor state
stopStop a running actor
treeShow actor hierarchy as a tree
validateValidate an actor manifest
watchWatch 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

  1. Create a new actor project

    theater create my-actor
    cd my-actor
    
  2. Build the actor

    theater build
    
  3. Start a Theater server (in another terminal)

    theater server
    
  4. Start the actor

    theater start manifest.toml
    
  5. Watch for changes and automatically redeploy

    theater watch . --manifest manifest.toml
    

Monitoring and Debugging

To monitor and debug actors:

  1. List all running actors

    theater list
    
  2. View actor logs

    theater logs <actor-id>
    
  3. Inspect actor state

    theater state <actor-id>
    
  4. View actor events

    theater events <actor-id>
    
  5. 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 -
    
  6. Restart an actor if issues occur

    theater restart <actor-id>
    

Advanced Usage

HTTP Actor Setup

For HTTP actors:

  1. Create an HTTP actor project

    theater create my-http-actor --template http
    
  2. Build and start

    cd my-http-actor
    theater build
    theater start manifest.toml
    
  3. The HTTP server will be available at the port specified in the manifest

Supervisor Pattern

For parent-child actor relationships:

  1. Create parent and child actors
  2. Configure the parent with supervisor capabilities
  3. Start the parent actor
  4. 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 actors
  • inspect <id> - Show detailed information about an actor
  • state <id> - Show the current state of an actor
  • events <id> - Show events for an actor
  • start <path> - Start an actor from a manifest
  • stop <id> - Stop a running actor
  • restart <id> - Restart a running actor
  • message <id> <msg> - Send a message to an actor
  • clear - Clear the screen
  • help - Show help
  • exit - 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 and theater <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:

  1. Begin with simple data transformation actors
  2. Progress to actors that maintain internal state
  3. 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:

  1. Each microservice is implemented as a Theater actor
  2. The services communicate through well-defined message interfaces
  3. A supervision hierarchy ensures system stability
  4. 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(&current_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

  1. 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,
}
}
  1. 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...
}
}
  1. 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

  1. Use the runtime log function liberally
  2. Test with different message types
  3. Verify state transitions
  4. Handle all error cases
  5. Monitor the hash chain
  6. Test all handler interfaces

Common Pitfalls

  1. Not Handling JSON Errors

    • Always handle deserialization errors
    • Validate JSON structure
    • Handle missing fields
  2. State Inconsistency

    • Validate state after changes
    • Keep state updates atomic
    • Handle partial updates
  3. Missing Error Logging

    • Log all errors
    • Include context
    • Track error patterns
  4. 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:

  1. Operation initiation
  2. Progress checking
  3. Result collection

Best Practices

  1. Event Buffering

    • Buffer events when they can't be processed immediately
    • Implement reasonable buffer limits
    • Handle buffer overflow gracefully
  2. Resource Management

    • Track resource usage carefully
    • Implement proper cleanup mechanisms
    • Handle resource exhaustion gracefully
  3. Error Handling

    • Propagate errors appropriately
    • Maintain system stability during errors
    • Log errors with context for debugging
  4. 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

  1. 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(&params)?
    }
  2. 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
    }
  3. 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(&params)?
    match self.call_func(store, state, params_deserialized).await ...
    }
  4. 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

  1. WebAssembly Layer: The function returns a result containing the new state and return value.

  2. Adapter Layer: The result is serialized back to JSON bytes:

    #![allow(unused)]
    fn main() {
    let result_serialized = serde_json::to_vec(&result)?
    }
  3. 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);
    }
  4. 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

  1. 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)
  2. Update In-Progress List

    • Add your proposal to /changes/in-progress.md
    • Include the proposal file name and a brief description
  3. 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
  4. 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

  1. Be explicit about your reasoning
  2. Document both successful and unsuccessful approaches
  3. Include relevant code examples or diagrams
  4. Reference related issues or discussions
  5. 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:

  1. System Architecture: The overall design and component relationships
  2. Data Flow: How information moves through the system
  3. Implementation Details: Technical specifics of key subsystems
  4. ID System: How actors and resources are identified
  5. Interface System: How components communicate
  6. State Management: Implementation of the state storage mechanisms
  7. 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.

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

  1. 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
  2. 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
  3. 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:

  1. Isolation through WebAssembly:

    • Actors run in sandboxed environments
    • Component model provides capability-based security
  2. Explicit State Management:

    • All state is explicitly managed through the Store
    • No hidden or shared state between actors
  3. Explicit Communication:

    • All communication happens through messages
    • No direct actor-to-actor function calls
  4. Comprehensive Tracing:

    • All system actions are recorded
    • Chain provides cryptographic verification
  5. 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:

  1. CLI or parent actor requests actor creation
  2. Runtime processes request
  3. Store retrieves component bytes
  4. Actor Executor instantiates component
  5. Runtime initializes actor state
  6. Event Chain records creation

Message Processing Flow

The sequence of component interactions during message processing:

  1. Message arrives at Runtime
  2. Runtime records message in Event Chain
  3. Runtime delivers message to target actor
  4. Actor Executor invokes appropriate handler
  5. Actor may access state via Store
  6. Actor response is recorded in Event Chain
  7. Response is delivered to sender

Failure Handling Flow

The sequence of component interactions during failure handling:

  1. Actor Executor detects failure
  2. Runtime records failure in Event Chain
  3. Runtime notifies supervisor
  4. Supervisor decides on action
  5. Runtime implements supervisory action
  6. 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:

  1. HTTP request arrives at Theater Server
  2. Server parses request and identifies target actor
  3. Request is converted to a message
  4. Message is recorded in Event Chain
  5. Message is queued for delivery to actor

CLI Commands

For CLI-initiated actions:

  1. User issues command via CLI
  2. CLI connects to Theater Server
  3. Command is converted to management message
  4. Message is recorded in Event Chain
  5. Server processes management command

Message Delivery

For actor-to-actor messaging:

  1. Sender actor invokes messaging API
  2. Message is recorded in Event Chain
  3. Message is queued for delivery
  4. Target actor receives message

Actor Processing

State Retrieval

When actors access state:

  1. Actor invokes Store API
  2. Runtime validates access request
  3. State request is recorded in Event Chain
  4. Store retrieves state data
  5. Data is returned to actor

Computation

During actor computation:

  1. Actor processes message data
  2. Actor may access state via Store
  3. Actor may use handlers for external services
  4. All handler invocations are recorded in Event Chain

State Updates

When actors modify state:

  1. Actor creates new state data
  2. Actor invokes Store API to save state
  3. Update request is recorded in Event Chain
  4. Store validates and persists new state
  5. Store returns new state reference to actor

External Output Processing

HTTP Responses

For HTTP response generation:

  1. Actor produces response data
  2. Response is recorded in Event Chain
  3. Response is converted to HTTP format
  4. HTTP response is sent to client

Event Streaming

For event subscription:

  1. Client subscribes to events via Theater Server
  2. New events are recorded in Event Chain
  3. Server filters events based on subscription
  4. Matching events are streamed to client

Special Data Flows

Supervision

During supervision operations:

  1. Runtime detects actor failure
  2. Failure is recorded in Event Chain
  3. Supervisor actor is notified
  4. Supervisor decides on action
  5. Supervisory action is recorded in Event Chain
  6. Runtime implements supervisory action

Replay

During event replay:

  1. Replay request identifies starting point
  2. Events are retrieved from Event Chain
  3. Actor state is initialized
  4. Events are applied sequentially
  5. 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

  1. Never Generate IDs Manually

    • Always use TheaterId::generate() to ensure proper randomness
  2. Store Full IDs

    • Don't truncate or modify IDs as this reduces their uniqueness properties
  3. Use Type Safety

    • Prefer the TheaterId type over raw strings when possible
    • This provides compile-time guarantees and better error handling
  4. Handle Parse Errors

    • Always check for errors when parsing IDs from strings
    • Invalid IDs should be treated as authentication/authorization failures
  5. 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 the v4 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:

  1. Secure Random ID System: A new system using 16-byte random IDs with base64url encoding
  2. Host CSPRNG Integration: Using the host system's cryptographically secure random number generator
  3. Improved Format: Shorter string representation (22 characters vs 36 for UUIDs)
  4. 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

  1. actor.wit - Basic actor interface:
interface actor {
    use types.{state};
    
    init: func(state: state, params: tuple<string>) -> result<tuple<state>, string>;
}
  1. 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>;
}
  1. 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>;
}
  1. 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>;
    // ...
}
  1. 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:

  1. Message Server Handler:

    • Handles direct actor-to-actor messaging
    • Supports both request/response and one-way sends
    • Serializes messages as JSON bytes
  2. HTTP Server Handler:

    • Exposes actor functionality via HTTP endpoints
    • Converts HTTP requests to actor messages
    • Transforms responses back to HTTP
  3. 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

  1. 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
  2. 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

HTTP Integration

  1. 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
  2. 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

Interface Implementation

Actors implement interfaces through WebAssembly components:

Required Component Structure

A Theater actor component must:

  1. Implement required interfaces (based on handlers)
  2. Export interface functions with correct signatures
  3. Handle state consistently
  4. 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:

  1. 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
  2. State Updates:

    • Functions return new state
    • Changes are recorded in hash chain
    • State is available for inspection and verification
  3. 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:

  1. Implement Multiple Interfaces:

    • Core actor functionality
    • Message handling
    • HTTP serving
    • Custom functionality
  2. Depend on Host Interfaces:

    • Message sending
    • HTTP client
    • Supervision
    • File system access
  3. 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:

  1. Tracing:

    • All interface calls are logged
    • State transitions are recorded
    • Message flow can be traced end-to-end
  2. Interface Inspection:

    • WIT interfaces can be introspected
    • Available functions can be listed
    • Type checking for message formats
  3. 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:

  1. WIT Definition:

    • Define interface functions and types
    • Document expected behavior
    • Specify state handling patterns
  2. Handler Implementation:

    • Create handler in Theater runtime
    • Connect WIT interface to actor
    • Handle message routing correctly
  3. Actor Implementation:

    • Implement interface functions
    • Handle state properly
    • Process messages according to spec

Best Practices

  1. Interface Design

    • Keep interfaces focused on single responsibility
    • Use clear, descriptive function names
    • Document expected behavior
    • Provide meaningful error messages
    • Consider versioning strategy
  2. Message Design

    • Use consistent type field for categorization
    • Include action field for specific operations
    • Structure payloads logically
    • Add metadata for debugging
    • Handle errors consistently
  3. State Management

    • Keep state serializable
    • Handle state transitions atomically
    • Validate state after changes
    • Consider state size impacts
    • Test state rollback scenarios
  4. 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:

  1. Connect Actors with Host Capabilities: Handlers expose host functions to WebAssembly actors, allowing them to interact with the host system and other actors
  2. Process Messages and Events: Handlers receive and process incoming messages, translating them into actor function calls
  3. Maintain Chain Integrity: All handler operations are recorded in the actor's chain, maintaining the verifiable history
  4. Provide Standard Interfaces: Handlers implement standard WebAssembly Interface Type (WIT) interfaces, ensuring consistency across actors

Handler Architecture

Each handler consists of two main parts:

  1. Host Functions (Imports): Functions provided by the host environment that actors can call. These are integrated into the actor's WebAssembly module during instantiation.

  2. 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:

  1. Registration: Handlers specified in the actor's manifest are registered with the actor runtime
  2. Initialization: Each handler is initialized and connected to the actor component
  3. Host Function Setup: The handler adds its host functions to the actor's WebAssembly linker
  4. Export Function Registration: The handler registers the actor's exported functions for callbacks
  5. Start: The handler's event loop is started in a separate task

During operation:

  1. Message Processing: Handlers receive messages or events and process them
  2. Function Calls: Handlers call actor functions or respond to actor requests to host functions
  3. State Recording: All interactions are recorded in the actor's state chain

During shutdown:

  1. Graceful Termination: Handlers receive shutdown signals and perform cleanup
  2. 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 identifier
  • config: Handler-specific configuration options

Available Handler Types

Theater provides several built-in handler types:

  1. message-server: Enables actor-to-actor messaging using both synchronous (request/response) and asynchronous (fire-and-forget) patterns
  2. http-client: Allows actors to make HTTP requests to external services
  3. http-framework: Exposes actor functionality via HTTP endpoints
  4. filesystem: Provides access to the filesystem with appropriate sandboxing
  5. supervisor: Enables parent-child actor relationships for supervision
  6. store: Provides content-addressable storage for actors
  7. runtime: Gives access to runtime information and operations
  8. 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:

  1. Implement the Handler trait
  2. Handle setup of host functions
  3. Process messages and events
  4. Call actor functions when needed
  5. 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:

  1. Sandboxed Access: Handlers provide controlled access to host resources
  2. Verifiable State: All handler operations are recorded in the chain
  3. Explicit Permissions: Actors must explicitly declare which handlers they use

Developing Custom Handlers

To develop a new handler for Theater:

  1. Define the WIT Interface: Create a new .wit file defining the interface
  2. Implement the Handler: Create a new handler implementation in Rust
  3. Register the Handler: Add the handler to the configuration system
  4. Connect to Actor Runtime: Integrate the handler with the actor runtime

Best Practices

  1. Handler Selection: Only include handlers that your actor actually needs
  2. Resource Management: Configure appropriate resource limits for handlers
  3. Error Handling: Implement proper error handling for handler operations
  4. 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

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:

  1. message-server-host: Functions that actors can call to send messages to other actors
  2. 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:

  1. Invalid Actor ID: When the target actor doesn't exist
  2. Delivery Failure: When the message can't be delivered
  3. Processing Error: When the target actor fails to process the message
  4. 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:

  1. Receives incoming messages from the actor's mailbox
  2. Processes messages based on their type
  3. Calls the appropriate actor function
  4. Updates the actor's state
  5. Returns responses (for request messages)
  6. Records all operations in the state chain

Channel Management

Channels have additional lifecycle management:

  1. Channel Creation: A unique channel ID is generated for each channel
  2. Channel Acceptance: The target actor must explicitly accept the channel
  3. Channel State: The handler tracks which channels are open
  4. Channel Closure: Either participant can close the channel

Best Practices

  1. Message Structure: Use consistent message structures with clear type and action fields
  2. Error Handling: Always handle potential errors from message operations
  3. Channel Management: Close channels when they're no longer needed
  4. State Design: Keep message handlers focused on state transitions
  5. Timeout Handling: Consider implementing timeouts for request operations

Security Considerations

  1. Message Validation: Always validate incoming messages before processing
  2. Actor ID Verification: Verify actor IDs before sending messages
  3. Payload Size: Be mindful of message payload sizes
  4. 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(&params.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(&params.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,)))
    }
}
}

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:

  1. Send HTTP requests to external services
  2. Process HTTP responses
  3. Record all HTTP interactions in the state chain
  4. 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 URL
  • headers: A list of HTTP headers as key-value pairs
  • body: 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 code
  • headers: A list of response headers as key-value pairs
  • body: 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:

  1. HttpClientRequestCall: Records when a request is made, including:

    • HTTP method
    • Target URL
    • Headers count
    • Body size
  2. HttpClientRequestResult: Records the result of a request, including:

    • Status code
    • Headers count
    • Body size
    • Success indicator
  3. 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:

  1. Invalid Method: When an invalid HTTP method is specified
  2. Network Errors: When network issues prevent the request from completing
  3. Timeout Errors: When the request times out
  4. 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:

  1. URL Validation: Validate URLs before making requests to prevent SSRF attacks
  2. Sensitive Data: Be careful with sensitive data in requests, as they are recorded in the state chain
  3. Authentication: Use secure methods for authentication in external APIs
  4. TLS Verification: The handler performs TLS verification by default
  5. Timeouts: Set appropriate timeouts for requests to prevent resource exhaustion

Implementation Details

Under the hood, the HTTP Client Handler:

  1. Converts the HttpRequest into a reqwest client request
  2. Sets up headers, body, and method
  3. Executes the request asynchronously
  4. Processes the response into an HttpResponse
  5. Records all operations in the state chain
  6. 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:

  1. No Direct Streaming: Large responses are loaded fully into memory
  2. No WebSocket Support: For WebSocket connections, use a dedicated WebSocket client
  3. No Client Certificate Authentication: TLS client certificates are not currently supported
  4. No Direct Proxy Configuration: Proxy settings cannot be configured per-request

Best Practices

  1. Error Handling: Always handle errors from HTTP requests properly
  2. Response Size: Be mindful of response sizes to avoid memory issues
  3. Request Rate: Implement rate limiting for external API calls
  4. Timeout Handling: Set appropriate timeouts for your use case
  5. Idempotency: Design requests to be idempotent when possible
  6. 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())
    }
}
}

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:

  1. A way for actors to receive and respond to HTTP requests
  2. Conversion between HTTP requests and actor-friendly formats
  3. Automatic state chain recording of all HTTP interactions
  4. 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 URI
  • path: The path component of the URI
  • query: Optional query string
  • headers: A list of HTTP headers as key-value pairs
  • body: 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 code
  • headers: A list of response headers as key-value pairs
  • body: 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:

  1. HttpFrameworkRequestCall: Records when a request is received, including:

    • HTTP method
    • Path
    • Headers count
    • Body size
  2. HttpFrameworkRequestResult: Records the result of processing a request, including:

    • Status code
    • Headers count
    • Body size
    • Processing time
  3. 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:

  1. Framework-Level Errors: Handled by the framework itself, such as:

    • Routing errors
    • Method not allowed
    • Actor not found
    • Malformed requests
  2. 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:

  1. Input Validation: Always validate and sanitize all HTTP request data
  2. Authentication: Implement proper authentication for protected endpoints
  3. Rate Limiting: Consider rate limiting to prevent abuse
  4. Error Information: Be careful about exposing error details in production
  5. CORS Policies: Implement appropriate CORS headers for browser security
  6. Content Security: Set proper content security policies

Implementation Details

Under the hood, the HTTP Framework Handler:

  1. Receives HTTP requests from the Theater HTTP server
  2. Converts them to the HttpRequest format
  3. Retrieves the current actor state
  4. Calls the actor's handle-request function
  5. Updates the actor's state with the new state
  6. Converts the HttpResponse back to an HTTP response
  7. Records all operations in the state chain
  8. Returns the response to the client

Best Practices

  1. RESTful Design: Follow RESTful principles for API design
  2. Stateless Design: Keep HTTP handlers as stateless as possible
  3. Error Handling: Implement proper error handling with appropriate status codes
  4. Content Types: Set appropriate Content-Type headers
  5. Validation: Validate all incoming data
  6. Testing: Test all endpoints with various input scenarios
  7. 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(&current_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()),
                }
            ))
        }
    }
}
}

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:

  1. Read and write files securely
  2. Create and manage directories
  3. Get file metadata
  4. List directory contents
  5. 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 subdirectories
  • new_dir: (Optional) If true, creates a new directory in /tmp/theater for the actor; if false, uses the specified path directly
  • allowed_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:

  1. FilesystemOperation: Records details of the operation:

    • Operation type (read, write, list, etc.)
    • Path
    • Size (for read/write operations)
    • Success/failure status
  2. 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:

  1. Permission Errors: When trying to access files outside the allowed path
  2. Not Found Errors: When a file or directory doesn't exist
  3. IO Errors: When read/write operations fail
  4. Format Errors: When paths are invalid
  5. 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:

  1. Path Configuration: Set the base path to limit file access
  2. Limited Permissions: Use allowed_commands to restrict operations
  3. Input Validation: Validate file paths before using them
  4. Content Validation: Validate file contents before writing
  5. Error Handling: Properly handle all error cases
  6. Resource Limits: Be mindful of file sizes and disk usage

Implementation Details

Under the hood, the Filesystem Handler:

  1. Validates all paths against the configured base directory
  2. Translates WIT interface calls to Rust's standard library file operations
  3. Handles errors and security checks
  4. Records all operations in the state chain
  5. Manages file handles and resources properly

Limitations

The current Filesystem Handler implementation has some limitations:

  1. No Streaming: Large files are loaded fully into memory
  2. No Symbolic Link Following: Symbolic links are not followed
  3. Limited Metadata: Some platform-specific file metadata is not available
  4. No File Locking: Concurrent access is not protected by file locks
  5. No Special Files: Device files, sockets, etc. are not supported

Best Practices

  1. Path Management: Use relative paths within your base directory
  2. Error Handling: Always handle file operation errors properly
  3. Resource Cleanup: Clean up temporary files when they're no longer needed
  4. Directory Structure: Create a clear directory structure for your actor's data
  5. Rate Limiting: Implement rate limiting for frequent file operations
  6. 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
    }
}
}

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:

  1. Spawn new child actors
  2. Monitor child actor lifecycle events
  3. Access child actor state and events
  4. Stop and restart child actors
  5. 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:

  1. SupervisorOperation: Records details of supervision operations:

    • Operation type (spawn, stop, restart, etc.)
    • Child actor ID
    • Result (success/failure)
  2. 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:

  1. Spawn Errors: When child actor creation fails
  2. Stop Errors: When child actor termination fails
  3. Restart Errors: When child actor restart fails
  4. Not Found Errors: When the specified child actor doesn't exist
  5. Access Errors: When accessing child state or events fails

Security Considerations

When using the Supervisor Handler, consider the following security aspects:

  1. Child Isolation: Child actors run in separate WebAssembly sandboxes
  2. State Access Controls: Only direct parent actors can access child state
  3. Manifest Validation: Validate manifests before spawning actors
  4. Resource Limits: Consider setting limits on child actor resource usage
  5. Privilege Separation: Design actor hierarchies with security in mind

Implementation Details

Under the hood, the Supervisor Handler:

  1. Communicates with the Theater runtime to manage child actors
  2. Tracks parent-child relationships in the actor system
  3. Routes supervision commands to the appropriate actors
  4. Manages actor processes and mailboxes
  5. Handles actor lifecycle events
  6. 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

  1. Hierarchical Design: Design clear supervision hierarchies
  2. Failure Domains: Group related actors under the same supervisor
  3. Restart Strategies: Choose appropriate restart strategies for different components
  4. State Recovery: Design child actors to recover gracefully from restarts
  5. Error Handling: Handle supervision errors properly
  6. Monitoring: Implement monitoring for supervisor decisions
  7. 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(&params));
    
    // 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(),
    })
}
}

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:

  1. Create and manage store instances
  2. Store and retrieve data using content-addressable storage
  3. Create and manage labels for easy content reference
  4. Check for content existence and calculate storage size
  5. Efficiently deduplicate content
  6. 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:

  1. Actor-Specific Labels: Prefix labels with the actor ID or name

    actor:12345:config
    
  2. Versioned Labels: Include version information in labels

    config:v1.0
    
  3. Type Labels: Include content type in the label

    image:logo
    
  4. 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:

  1. Storage Errors: When content storage fails
  2. Retrieval Errors: When content retrieval fails
  3. Label Errors: When label operations fail
  4. Not Found Errors: When content or labels don't exist
  5. IO Errors: When disk operations fail

Security Considerations

When using the Store Handler, consider the following security aspects:

  1. Content Validation: Validate data before storing it
  2. Label Namespaces: Use namespaced labels to avoid conflicts
  3. Size Limits: Be mindful of storage size and implement limits
  4. Sensitive Data: Consider encrypting sensitive data before storage
  5. Cleanup: Implement policies for removing unused content

Implementation Details

Under the hood, the Store Handler:

  1. Uses SHA-1 hashing to create unique content identifiers
  2. Stores content in a directory structure organized by store ID
  3. Maintains separate directories for content and label mappings
  4. Records detailed events for all operations
  5. 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

  1. Store Management: Create separate stores for different use cases
  2. Content Size: The store is optimized for small to medium content sizes (< 10MB)
  3. Reference Tracking: Keep track of content references for important data
  4. Label Schemes: Develop consistent label naming schemes
  5. Cleanup: Implement periodic cleanup for unused content
  6. Error Handling: Always handle store operation errors appropriately
  7. 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))
    }
}
}

Event Types Reference

The Store Handler tracks detailed events for all operations. Here's a complete reference of the event types:

Call Events

Event TypeDescriptionParameters
NewStoreCallCalled when creating a new storeNone
StoreCallCalled when storing contentstore_id, content
GetCallCalled when retrieving contentstore_id, content_ref
ExistsCallCalled when checking if content existsstore_id, content_ref
LabelCallCalled when labeling contentstore_id, label, content_ref
GetByLabelCallCalled when getting content by labelstore_id, label
StoreAtLabelCallCalled when storing and labeling contentstore_id, label, content
ReplaceContentAtLabelCallCalled when replacing content at a labelstore_id, label, content
ReplaceAtLabelCallCalled when replacing a reference at a labelstore_id, label, content_ref
RemoveLabelCallCalled when removing a labelstore_id, label
ListLabelsCallCalled when listing all labelsstore_id
ListAllContentCallCalled when listing all contentstore_id
CalculateTotalSizeCallCalled when calculating total sizestore_id

Result Events

Event TypeDescriptionParameters
NewStoreResultResult of creating a new storestore_id, success
StoreResultResult of storing contentstore_id, content_ref, success
GetResultResult of retrieving contentstore_id, content_ref, content, success
ExistsResultResult of checking if content existsstore_id, content_ref, exists, success
LabelResultResult of labeling contentstore_id, label, content_ref, success
GetByLabelResultResult of getting content by labelstore_id, label, content_ref, success
StoreAtLabelResultResult of storing and labeling contentstore_id, label, content_ref, success
ReplaceContentAtLabelResultResult of replacing content at a labelstore_id, label, content_ref, success
ReplaceAtLabelResultResult of replacing a reference at a labelstore_id, label, content_ref, success
RemoveLabelResultResult of removing a labelstore_id, label, success
ListLabelsResultResult of listing all labelsstore_id, labels, success
ListAllContentResultResult of listing all contentstore_id, content_refs, success
CalculateTotalSizeResultResult of calculating total sizestore_id, size, success

Error Events

Event TypeDescriptionParameters
ErrorRecords an error with any operationoperation, 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:

  1. Access information about themselves and the runtime
  2. Control their lifecycle
  3. Get system and environment information
  4. Record custom metrics and events
  5. 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:

  1. RuntimeOperation: Records runtime operations like environment variable access or system info requests
  2. CustomEvent: Records user-defined events with their data
  3. LogEvent: Records log messages with their level
  4. MetricEvent: Records custom metrics with their values

Error Handling

The Runtime Handler provides error information for various failure scenarios:

  1. Log Errors: When logging fails
  2. Metric Errors: When metric recording fails
  3. Event Errors: When custom event recording fails
  4. Environment Errors: When environment variable access fails

Security Considerations

When using the Runtime Handler, consider the following security aspects:

  1. Environment Variables: Be careful with sensitive environment variables
  2. Logging: Don't log sensitive data like passwords or tokens
  3. Metrics: Avoid using personally identifiable information in metric names
  4. Custom Events: Be mindful of the data included in custom events
  5. System Information: Consider what system information is exposed to actors

Implementation Details

Under the hood, the Runtime Handler:

  1. Provides a bridge between WebAssembly actors and the host runtime
  2. Translates WIT interface calls to host runtime operations
  3. Records all operations in the state chain
  4. Manages access to system resources and information
  5. 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

  1. Consistent Logging: Use consistent log levels and formats
  2. Meaningful Metrics: Design metrics that provide actionable insights
  3. Error Handling: Always handle errors from runtime functions
  4. Resource Usage: Be mindful of resource usage in metrics collection
  5. Security: Never log sensitive information

Performance Considerations

  1. Logging Overhead: Excessive logging can impact performance
  2. Metric Cardinality: Too many unique metric names can cause issues
  3. Event Size: Large event payloads may impact performance
  4. System Info Calls: Frequent system info calls may have overhead

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:

  1. Introduce controlled delays in their execution
  2. Implement timeout patterns for operations
  3. Enforce rate limits and throttling
  4. 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:

  1. TimingOperation: Records details of timing operations:

    • Operation type (sleep, now, performance-now)
    • Duration (for sleep operations)
    • Timestamp
  2. 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:

  1. Duration Errors: When sleep duration is outside allowed range
  2. Operation Errors: When timing operations fail
  3. Resource Errors: When system resources are unavailable

Security Considerations

When using the Timing Handler, consider the following security aspects:

  1. Sleep Limits: The configuration enforces limits on sleep durations
  2. Resource Consumption: Long or frequent sleeps may impact system resources
  3. Timing Attacks: Be aware of potential timing side-channel attacks

Implementation Details

Under the hood, the Timing Handler:

  1. Uses the Tokio runtime for asynchronous sleep operations
  2. Enforces configurable minimum and maximum sleep durations
  3. Records all operations in the state chain
  4. Provides consistent time sources across the actor system

Performance Considerations

  1. Sleep Overhead: There is a small overhead for each sleep operation
  2. Time Resolution: Time functions have platform-dependent resolution
  3. Resource Usage: Excessive sleep operations may impact system performance

Best Practices

  1. Error Handling: Always handle errors from timing functions
  2. Sleep Duration: Use reasonable sleep durations
  3. Batch Processing: Consider batching operations instead of sleeping between each
  4. Timeouts: Implement timeouts for operations that may not complete
  5. 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)
})?;
}

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:

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(&current_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(&current_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

  1. State Management

    • Use serde for state serialization
    • Keep state JSON-serializable
    • Include timestamps in state
    • Handle serialization errors
  2. Message Handling

    • Validate message format
    • Handle all message types
    • Return consistent responses
    • Preserve state on errors
  3. Handler Implementation

    • Implement appropriate interfaces
    • Handle all request types
    • Return proper responses
    • Maintain state consistency
  4. Error Handling

    • Log errors with context
    • Return unchanged state on error
    • Validate all inputs
    • Handle all error cases

Development Tips

  1. Use the chat-room example as a reference implementation
  2. Test with multiple handler types
  3. Monitor the hash chain during development
  4. Use logging for debugging
  5. Validate state transitions