Skip to content

Context & State

Overview

one of development phases of the App Engine introduced two foundational systems that every other subsystem builds on:

flowchart TD
    R["Runtime"] --> CM["ContextManager"]
    R --> SM["StateManager"]
    CM --> C["Context<br/>(identity + environment)"]
    SM --> S["State<br/>(current condition)"]

These are deliberately separate:

Context = "Where am I executing?"
State   = "What is the current condition?"

Context describes the environment in which something is executing. State describes the condition of that environment. They are not the same thing, and merging them causes architectural confusion.


Context Hierarchy

Contexts form a parent-child hierarchy:

flowchart TD
    A["ApplicationContext"] --> B["SessionContext"]
    B --> C["ExecutionContext"]
Context Lifetime Answers
ApplicationContext Entire application run "Which application instance is this?"
SessionContext A logical session within the app "Which session/user/workspace is this?"
ExecutionContext One particular operation "Which execution is this?"

A session belongs to an application. An execution belongs to a session. This hierarchy is enforced by the ContextManager.


Creating Contexts

use app_shell::app_engine::{
    AppEngine, Bootstrap, Lifecycle,
    ApplicationId, SessionId, ExecutionId,
};

let mut engine = Bootstrap::create()?;

// ApplicationContext is created automatically by Bootstrap
let app_id = engine.context().id();
println!("Application: {}", app_id.value());

// Create a session under the application
let session_id = engine
    .context_manager_mut()
    .create_session_context()?;

// Create an execution under the session
let exec_id = engine
    .context_manager_mut()
    .create_execution_context(session_id)?;

// Verify hierarchy
let session = engine.context_manager().session_context(session_id).unwrap();
let execution = engine.context_manager().execution_context(exec_id).unwrap();

assert_eq!(session.application_id(), app_id);   // session → app
assert_eq!(execution.session_id(), session_id); // execution → session

Multiple Sessions

An application can have multiple sessions:

let session_a = engine.context_manager_mut().create_session_context()?;
let session_b = engine.context_manager_mut().create_session_context()?;
let session_c = engine.context_manager_mut().create_session_context()?;

assert_eq!(engine.context_manager().session_count(), 3);
assert_ne!(session_a, session_b);
assert_ne!(session_b, session_c);

Each session could represent a different user, workspace, CLI invocation, or UI window — the App Engine doesn't need to know which.

Multiple Executions Per Session

let session = engine.context_manager_mut().create_session_context()?;

let exec_1 = engine.context_manager_mut().create_execution_context(session)?;
let exec_2 = engine.context_manager_mut().create_execution_context(session)?;
let exec_3 = engine.context_manager_mut().create_execution_context(session)?;

assert_eq!(engine.context_manager().execution_count(), 3);

// All executions belong to the same session
let e1 = engine.context_manager().execution_context(exec_1).unwrap();
let e2 = engine.context_manager().execution_context(exec_2).unwrap();
assert_eq!(e1.session_id(), session);
assert_eq!(e2.session_id(), session);

Error: Execution Without Session

use app_shell::app_engine::ContextError;

// Can't create a session without an application context
let mut mgr = ContextManager::new();
let result = mgr.create_session_context();
assert!(matches!(result, Err(ContextError::NoApplicationContext)));

// Can't create an execution without a valid session
let mut mgr = ContextManager::new();
mgr.create_application_context("app");
let result = mgr.create_execution_context(SessionId::new()); // bogus session
assert!(matches!(result, Err(ContextError::SessionNotFound)));

Context Identity

Each context has a typed, stable, copyable ID:

let app_id: ApplicationId = engine.context().id();
let session_id: SessionId = engine.context_manager_mut().create_session_context()?;
let exec_id: ExecutionId = engine.context_manager_mut().create_execution_context(session_id)?;

// IDs are Copy — pass them around freely
let copied = app_id;
assert_eq!(app_id, copied);

// IDs are unique
let another_app = ApplicationId::new();
assert_ne!(app_id, another_app);

// IDs are hashable (usable as HashMap keys)
let mut map = std::collections::HashMap::new();
map.insert(app_id, "root");

Specialized Contexts

Beyond the core hierarchy, other subsystems define their own context types that reference the core context IDs:

flowchart TD
    A["ApplicationContext"] --> B["SessionContext"]
    B --> C["ExecutionContext"]
    C --> D["TaskContext"]
    C --> E["CommandContext"]
    C --> F["PluginContext"]

TaskContext

use app_shell::app_engine::{
    ExecutionId, TaskContext, TaskId,
    Deadline, Duration,
};

let exec_id = ExecutionId::new();
let task_context = TaskContext::new(exec_id)
    .with_application(app_id)
    .with_session(session_id)
    .with_metadata("source", "scheduler")
    .with_deadline(Deadline::after(Duration::from_secs(30)));

// The TaskManager sets the task_id automatically on the context
let task_id = engine.task_manager().create(
    "project.export",
    task_context,
    TaskInput::new("/tmp/output.pdf".to_string()),
)?;

let task = engine.task_manager().get(task_id);
assert_eq!(task.context().execution_id(), exec_id);
assert_eq!(task.context().task_id(), Some(task_id));  // set by manager
assert!(task.context().is_expired() == false);  // deadline not yet passed

additions:

// Cancellation — check during long-running tasks
if task.context().is_cancelled() {
    return Ok(TaskResult::cancelled());
}

// Deadline — check for timeout
if task.context().is_expired() {
    return Err(TaskError::ExecutionFailed {
        id: 0, reason: "timeout".to_string()
    });
}

// Progress — report via output channel (emits signal on SignalBus)
task.context().output().progress(0.75);
task.context().output().push("Frame 3 rendered".to_string());

CommandContext

use app_shell::app_engine::{CommandContext, Command, CommandInput, ExecutionId};

let exec_id = ExecutionId::new();
let cmd_context = CommandContext::new(exec_id)
    .with_application(app_id)
    .with_session(session_id)
    .with_metadata("invoked_by", "cli")
    .with_deadline(Deadline::after(Duration::from_secs(10)));

let command = Command::new(
    "app.quit",
    cmd_context,
    CommandInput::empty(),
);

// The executor receives the context when executing
let result = engine.execute_command(&command)?;

PluginContext

// PluginContext is created by the PluginManager during initialize()
// The plugin receives it:

impl Plugin for MyPlugin {
    fn initialize(&mut self, ctx: &PluginContext, engine: &EngineRef) -> Result<(), PluginError> {
        println!("Plugin '{}' initializing", ctx.plugin_id());
        assert_eq!(ctx.plugin_id(), "my_plugin");
        Ok(())
    }
}

PluginContext is intentionally minimal — it provides the plugin's identity and engine metadata, not unlimited access to the runtime.


What Context Should NOT Contain

✓ execution_id, session_id, application_id
✓ cancellation flag, deadline, progress channel
✓ generic key-value metadata

✗ current document
✗ selected card
✗ active scene
✗ domain objects
✗ UI state

If you need to pass domain objects to a task, the domain code should resolve them through its own mechanisms — not stuff them into TaskContext.

// Good: domain data in TaskInput (type-erased payload)
let task_id = engine.task_manager_mut().create(
    "project.export",
    TaskContext::new(exec_id),
    TaskInput::new(ExportInput {
        document_path: "/docs/my_project.dp".to_string(),
        output_format: "pdf".to_string(),
    }),
)?;

// Bad: domain data in TaskContext
let context = TaskContext::new(exec_id)
    .with_metadata("document", "/docs/my_project.dp");  // ← domain leak!

State System

While Context describes where, State describes what is happening.

use app_shell::app_engine::{StateId, StateStore, StateManager, StateTransition, LifecycleState};

// The runtime mirrors its lifecycle state into the StateManager
let runtime_state_id = engine.runtime_state_id();

// Read current state
let current = engine.state_manager().current(runtime_state_id);
assert_eq!(current, Some(LifecycleState::Created));

// The lifecycle updates this automatically:
engine.initialize()?;
assert_eq!(
    engine.state_manager().current(runtime_state_id),
    Some(LifecycleState::Initialized)
);
engine.start()?;
assert_eq!(
    engine.state_manager().current(runtime_state_id),
    Some(LifecycleState::Running)
);

StateStore

StateStore<S> is a generic in-memory key-value state store. It is thread-safe (uses Mutex internally):

use app_shell::app_engine::{StateId, StateStore, LifecycleState};

let store: StateStore<LifecycleState> = StateStore::new();
let id = StateId::new(1);

// Write
store.write(id, LifecycleState::Running);

// Read
assert_eq!(store.read(id), Some(LifecycleState::Running));

// Check
assert!(store.contains(id));

// Remove
let removed = store.remove(id);
assert_eq!(removed, Some(LifecycleState::Running));
assert!(!store.contains(id));

Multiple independent state slots:

store.write(StateId::new(1), LifecycleState::Created);
store.write(StateId::new(2), LifecycleState::Running);
store.write(StateId::new(3), LifecycleState::Stopped);

assert_eq!(store.len(), 3);
assert_eq!(store.read(StateId::new(2)), Some(LifecycleState::Running));

State Transitions

A StateTransition<S> represents a change from one state to another:

use app_shell::app_engine::{StateTransition, LifecycleState};

let transition = StateTransition::new(
    LifecycleState::Created,
    LifecycleState::Initialized,
);

assert_eq!(transition.from(), &LifecycleState::Created);
assert_eq!(transition.to(), &LifecycleState::Initialized);
assert!(!transition.is_no_op());

// A no-op transition
let noop = StateTransition::new(LifecycleState::Running, LifecycleState::Running);
assert!(noop.is_no_op());

StateManager

StateManager<S> sits above StateStore and validates transitions. It is thread-safe (Mutex internally):

use app_shell::app_engine::{StateManager, StateId, StateTransition, LifecycleState, StateError};

let mgr: StateManager<LifecycleState> = StateManager::new();
let id = StateId::new(1);

// Set initial state
mgr.set(id, LifecycleState::Created);

// Valid transition: Created → Initialized
mgr.transition(id, LifecycleState::Created, LifecycleState::Initialized)?;
assert_eq!(mgr.current(id), Some(LifecycleState::Initialized));

// Invalid: from doesn't match current
let result = mgr.transition(id, LifecycleState::Created, LifecycleState::Running);
assert!(matches!(result, Err(StateError::TransitionMismatch { .. })));

// State is unchanged after failed transition
assert_eq!(mgr.current(id), Some(LifecycleState::Initialized));

Apply a StateTransition object

let transition = StateTransition::new(LifecycleState::Initialized, LifecycleState::Running);
mgr.apply_transition(id, &transition)?;
assert_eq!(mgr.current(id), Some(LifecycleState::Running));

Error: State Not Found

let result = mgr.apply_transition(StateId::new(999), &transition);
assert!(matches!(result, Err(StateError::StateNotFound)));

Context vs State

This distinction is critical:

Context State
"Execution #42 belongs to session #3" "Execution #42 is currently running"
ApplicationContext RuntimeState = Running
SessionContext TaskState = Pending
ExecutionContext JobState = Queued
TaskContext LifecycleState = Initialized
CommandContext ResourceState = Loaded
PluginContext ServiceState = Started

When to use Context

Use Context when you need to: - Identify where an operation is happening (which session, which execution). - Track parent relationships (this execution belongs to this session). - Pass execution metadata to handlers (source, priority hints). - Create isolated execution scopes (different sessions for different users). - Provide cancellation/deadline/progress to task handlers.

When to use State

Use State when you need to: - Track the current condition of something (is the runtime running? is the task pending?). - Validate transitions (can a completed task be restarted?). - Store generic runtime values that systems need to query. - Observe lifecycle changes (the StateManager mirrors runtime lifecycle).

When NOT to use either

Don't put in Context Don't put in State
Current document "The document has 37 layers"
Selected card "The selected card is Ace of Spades"
Active scene "The scene contains 1200 objects"
UI window reference "The window is 1920×1080"

Those are Domain State — they belong in the Domain Engine.


Domain State Boundary

✓ App Engine State ✗ Domain State
"Task #42 is running" "Document Foo has 37 shapes"
"Resource #7 is loaded" "Selected object is Rectangle"
"Service #3 is started" "Card database has 1200 entries"
"Runtime is in Running state" "Scene contains 5 lights"

The App Engine's StateManager can store generic state values (any type S: Clone + PartialEq + Debug). It can store LifecycleState, TaskState, ResourceState, etc. — but it should never store domain-specific data like "which document is open."

If a task needs to know "which document to export," that information comes through TaskInput (the task's data payload), not through TaskContext or StateManager:

// Good: domain data in TaskInput
let task_id = engine.task_manager_mut().create(
    "export.project",
    TaskContext::new(exec_id)
        .with_application(app_id)
        .with_session(session_id),
    TaskInput::new(ExportInput {
        document_path: "/docs/my_project.dp".to_string(),
        format: "pdf".to_string(),
        output_path: "/tmp/export.pdf".to_string(),
    }),
)?;

Thread Safety

Both StateStore and StateManager are thread-safe:

// StateStore uses Mutex internally
let store: StateStore<LifecycleState> = StateStore::new();
// All methods take &self (write() locks Mutex internally)

// StateManager uses Mutex (via StateStore)
let mgr: StateManager<LifecycleState> = StateManager::new();
// All methods take &self (set, current, transition, apply_transition)

// Both are Send + Sync
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<StateStore<LifecycleState>>();
assert_send_sync::<StateManager<LifecycleState>>();

This means handlers can read and write state through EngineRef (which provides &StateManager):

// In a task handler:
fn execute(&self, _input: &TaskInput, _ctx: &TaskContext, engine: &EngineRef) -> Result<TaskResult, TaskError> {
    // Read state
    let state_id = engine.state_manager().current(StateId::new(0));

    // Write state (thread-safe, Mutex internally)
    engine.state_manager().set(StateId::new(1), LifecycleState::Running);

    Ok(TaskResult::completed())
}

Integration with Runtime

The runtime owns both ContextManager and StateManager:

let mut engine = Bootstrap::create()?;

// Context: create sessions and executions
let session_id = engine.context_manager_mut().create_session_context()?;
let exec_id = engine.context_manager_mut().create_execution_context(session_id)?;

// State: the runtime's own lifecycle is mirrored
let state_id = engine.runtime_state_id();
engine.initialize()?;
assert_eq!(
    engine.state_manager().current(state_id),
    Some(LifecycleState::Initialized)
);
engine.start()?;
assert_eq!(
    engine.state_manager().current(state_id),
    Some(LifecycleState::Running)
);

Quick Reference

Context API

// Create
let app_id = engine.context().id();
let session_id = engine.context_manager_mut().create_session_context()?;
let exec_id = engine.context_manager_mut().create_execution_context(session_id)?;

// Query
engine.context_manager().application_context()          // → Option<&ApplicationContext>
engine.context_manager().session_context(session_id)    // → Option<&SessionContext>
engine.context_manager().execution_context(exec_id)     // → Option<&ExecutionContext>
engine.context_manager().session_count()                // → usize
engine.context_manager().execution_count()              // → usize

State API

// Store
let store: StateStore<LifecycleState> = StateStore::new();
store.write(StateId::new(1), LifecycleState::Running);
store.read(StateId::new(1));          // → Option<LifecycleState>
store.contains(StateId::new(1));      // → bool
store.remove(StateId::new(1));        // → Option<LifecycleState>

// Manager
let mgr: StateManager<LifecycleState> = StateManager::new();
mgr.set(StateId::new(1), LifecycleState::Created);
mgr.current(StateId::new(1));         // → Option<LifecycleState>
mgr.transition(id, from, to)?;        // validates + applies
mgr.apply_transition(id, &transition)?;

// Transition
let t = StateTransition::new(from, to);
t.from();      // → &S
t.to();        // → &S
t.is_no_op();  // → bool

Next Steps