Skip to main content

@tokenring-ai/app

Overview

Base application framework for TokenRing applications, providing service management, plugin architecture, and state management through a unified interface. The package serves as the foundational infrastructure for building modular, extensible TokenRing applications with comprehensive lifecycle management.

Key Features

  • Service-Oriented Architecture: Organizes functionality into configurable services with registry-based management
  • Plugin-Based Extensions: Seamless integration with plugin system for modular functionality
  • Type-Safe Configuration: Zod-based validation for all configuration schemas with layered config loading
  • Lifecycle Management: Controlled initialization, startup, and shutdown processes with automatic service restart on error
  • State Isolation: Separate state slices with serialization and deserialization support
  • Signal-Based Shutdown: Graceful termination using AbortSignal with progress indicator
  • Background Task Management: Automatic error handling for async background tasks with tracking
  • Comprehensive Logging: Structured output for system messages and errors with service context
  • Async State Subscriptions: Support for async state observation with abort handling
  • Service Auto-Restart: Services that exit unexpectedly are automatically restarted after 5 seconds
  • Session Checkpointing: Generate and restore app session checkpoints with state persistence
  • Human ID Generation: Automatic generation of human-readable session IDs

Core Components

TokenRingApp

The main application class that orchestrates services, configuration, and lifecycle management.

PluginManager

Manages plugin installation and lifecycle. Implements TokenRingService.

StateManager

Type-safe state management with serialization support.

buildTokenRingAppConfig

Configuration builder that loads from multiple locations with Zod validation. Creates data directory and .gitignore file if they don't exist.

AppLogsState

State slice for managing application logs with timestamp, level, and message.

Services

PluginManager

The PluginManager is automatically registered as a service when instantiated. It manages the lifecycle of all plugins in the application.

Service Name: PluginManager
Service Description: Manages plugins

Methods

  • getPlugins(): Returns all installed plugins
  • installPlugins(plugins): Installs and starts plugins with configuration validation
  • reconfigurePlugins(newConfig): Reconfigures plugins with new application configuration

Provider Documentation

This package does not use a provider architecture. It provides core application infrastructure that other packages build upon.

RPC Endpoints

This package does not define RPC endpoints. It provides core application infrastructure.

Chat Commands

This package does not define chat commands. Chat commands are typically defined by individual plugins.

Configuration

Application Configuration Schema

export const TokenRingAppConfigSchema = z.object({
app: z.object({
dataDirectory: z.string(),
configFileName: z.string(),
configSchema: z.custom<z.ZodTypeAny>(),
shutdownMonitorIntervalMs: z.number().default(2000),
serviceRestartDelayMs: z.number().default(5000),
})
});

export const LooseTokenRingAppConfigSchema = TokenRingAppConfigSchema.loose();
export type TokenRingAppConfig = z.output<typeof LooseTokenRingAppConfigSchema>;

Configuration Options:

PropertyTypeDefaultDescription
dataDirectorystring-Directory for application data storage
configFileNamestring-Base name for configuration files (without extension)
configSchemaZodTypeAny-Zod schema for validating application configuration
shutdownMonitorIntervalMsnumber2000Interval in milliseconds for monitoring shutdown progress
serviceRestartDelayMsnumber5000Delay in milliseconds before restarting a service that exited unexpectedly

Plugin Configuration Schema

import { z } from "zod";
import type { TokenRingPlugin } from "@tokenring-ai/app";

const MyPluginSchema = z.object({
enabled: z.boolean().default(true),
apiKey: z.string().optional(),
models: z.array(z.string()).default([])
});

const myPlugin: TokenRingPlugin<typeof MyPluginSchema> = {
name: "MyPlugin",
version: "1.0.0",
description: "Plugin with config",
config: MyPluginSchema,
install(app, config) {
// Config is already validated
if (config.enabled) {
// Initialize plugin
}
},
start(app, config) {
// Start service
},
reconfigure(app, config) {
// Handle config changes
}
};

Config Loading Order

Config files are loaded from ~ (home) and dataDirectory in that order, with extensions .ts, .mjs, .cjs, .js. The config is validated at each step to ensure it is complete and well-formed.

Additional Behavior:

  • Creates the data directory if it doesn't exist
  • Creates a .gitignore file in the data directory if it doesn't exist (with *.sqlite* pattern)
  • Merges configs using deepMerge from @tokenring-ai/utility
  • Validates the merged config at each step using configSchema.parse()

Integration

Service Registration

Services are registered with the application using the addServices method:

import { TokenRingService } from "@tokenring-ai/app";

class MyService implements TokenRingService {
name = "MyService";
description = "A custom service";

async start(signal: AbortSignal) {
// Initialization
}

async run(signal: AbortSignal) {
// Main loop
}

async stop() {
// Cleanup
}
}

app.addServices(new MyService());

Plugin Installation

Plugins should be installed via the PluginManager:

import PluginManager from "@tokenring-ai/app/PluginManager";
import type { TokenRingPlugin } from "@tokenring-ai/app";

const pluginManager = new PluginManager(app);

const myPlugin: TokenRingPlugin = {
name: "MyPlugin",
version: "1.0.0",
description: "Custom plugin",
install(app) {
// Set up plugin (cannot await)
},
start(app) {
// Start plugin (can await)
}
};

await pluginManager.installPlugins([myPlugin]);

Plugin Reconfiguration

import { z } from "zod";
import type { TokenRingPlugin } from "@tokenring-ai/app";

const MyPluginSchema = z.object({
enabled: z.boolean().default(true),
apiKey: z.string().optional()
});

const myPlugin: TokenRingPlugin<typeof MyPluginSchema> = {
name: "MyPlugin",
version: "1.0.0",
description: "Plugin with reconfiguration support",
config: MyPluginSchema,
install(app, config) {
// Initial setup
},
start(app, config) {
// Start service
},
reconfigure(app, config) {
// Handle config changes
}
};

await pluginManager.installPlugins([myPlugin]);

// Later, when app config changes:
const { restartRequired } = await pluginManager.reconfigurePlugins(newAppConfig);
if (restartRequired) {
console.log("Some plugins require restart");
}

State Serialization

import fs from "node:fs";
import StateManager from "@tokenring-ai/app/StateManager";

// Save state
const checkpoint = app.generateStateCheckpoint();
await fs.writeFile("checkpoint.json", JSON.stringify(checkpoint));

// Load state
const savedCheckpoint = JSON.parse(await fs.readFile("checkpoint.json", "utf-8"));
app.restoreState(savedCheckpoint.state);

Abort Signal Handling

const app = new TokenRingApp(config);

app.runBackgroundTask(service, async (signal) => {
while (!signal.aborted) {
const result = await longRunningOperation();
processResult(result);
}
});

// Stop the app
app.shutdown();

Service Attach/Detach

Services can optionally implement attach and detach methods to integrate with agents:

import type { TokenRingService } from "@tokenring-ai/app";
import type { Agent, AgentCreationContext } from "@tokenring-ai/agent";

class AgentAwareService implements TokenRingService {
name = "AgentAwareService";
description = "Service that integrates with agents";

attach(agent: Agent, creationContext: AgentCreationContext) {
// Attach to agent during creation
}

detach(agent: Agent) {
// Detach from agent
}
}

Usage Examples

Basic Application Setup

import TokenRingApp, { TokenRingAppConfigSchema } from "@tokenring-ai/app";
import { z } from "zod";

const config = {
app: {
dataDirectory: "/path/to/data",
configFileName: "app.config",
configSchema: z.object({ apiKey: z.string() }),
shutdownMonitorIntervalMs: 2000,
serviceRestartDelayMs: 5000,
}
};

const app = new TokenRingApp(config);

console.log(app.sessionId); // Unique human-readable session ID for this instance

Service Management

import TokenRingApp, { TokenRingService } from "@tokenring-ai/app";

class MyService implements TokenRingService {
name = "MyService";
description = "A custom service";

async start(signal: AbortSignal) {
console.log("MyService starting");
}

async run(signal: AbortSignal) {
console.log("MyService running");
while (!signal.aborted) {
// Do work
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

async stop() {
console.log("MyService stopping");
}

doSomething() {
return "Service result";
}
}

// Add service to application
app.addServices(new MyService());

// Get service by type
const myService = app.requireService(MyService);

// Start the application
await app.run();

Plugin with Configuration

import { z } from "zod";
import type { TokenRingPlugin } from "@tokenring-ai/app";

const MyPluginConfigSchema = z.object({
apiKey: z.string(),
model: z.string().default("gpt-3.5-turbo")
});

const myPlugin: TokenRingPlugin<typeof MyPluginConfigSchema> = {
name: "MyPlugin",
version: "1.0.0",
description: "My custom plugin with config",
config: MyPluginConfigSchema,
install(app, config) {
console.log(`Installing with API key: ${config.apiKey}`);
// Note: Cannot await here, use start() for async operations
},
start(app, config) {
console.log(`Starting with model: ${config.model}`);
},
reconfigure(app, config) {
console.log(`Reconfiguring with model: ${config.model}`);
}
};

// Add plugin to application via PluginManager
const pluginManager = new PluginManager(app);
await pluginManager.installPlugins([myPlugin]);

State Management

import StateManager from "@tokenring-ai/app/StateManager";
import { SerializableStateSlice } from "@tokenring-ai/app/StateManager";
import { z } from "zod";

const serializationSchema = z.object({
data: z.string(),
});

class UserStateSlice extends SerializableStateSlice<typeof serializationSchema> {
readonly name = "UserState";
serializationSchema = serializationSchema;
email: string;

constructor(public name: string, email: string) {
super("UserState", serializationSchema);
this.email = email;
}

serialize() {
return { name: this.name, email: this.email };
}

deserialize(data: z.output<typeof serializationSchema>) {
this.name = data.name;
this.email = data.email;
}
}

// Initialize state
const stateManager = new StateManager();
stateManager.initializeState(
UserStateSlice,
{ name: "John", email: "john@example.com" }
);

// Update state
const result = stateManager.mutateState(UserStateSlice, (state) => {
state.name = "Jane";
return state.name;
});

console.log(result); // "Jane"

// Subscribe to changes
const unsubscribe = stateManager.subscribe(UserStateSlice, (state) => {
console.log("State changed:", state);
});

// Async state observation
const signal = new AbortController().signal;
const stateStream = stateManager.subscribeAsync(UserStateSlice, signal);
for await (const state of stateStream) {
console.log("New state:", state);
}

// Wait for state predicate
await stateManager.waitForState(UserStateSlice, (state) => state.name === "Jane");

// Wait for state predicate with timeout
try {
await stateManager.timedWaitForState(UserStateSlice, (state) => state.name === "Jane", 5000);
} catch (error) {
console.log("Timeout waiting for state");
}

// Iterate over state entries
for (const slice of stateManager.slices()) {
console.log("State slice:", slice.name);
}

Build Config from Files

import buildTokenRingAppConfig from "@tokenring-ai/app/buildTokenRingAppConfig";
import { z } from "zod";
import { TokenRingAppConfigSchema } from "@tokenring-ai/app/TokenRingApp";

const AppConfigSchema = z.object({
apiKey: z.string(),
model: z.string().default("gpt-4")
});

const config = await buildTokenRingAppConfig({
app: {
dataDirectory: "/path/to/data",
configFileName: "app.config",
configSchema: AppConfigSchema,
shutdownMonitorIntervalMs: 2000,
serviceRestartDelayMs: 5000,
},
apiKey: "",
model: "gpt-3.5-turbo"
});

Plugin Manager Usage

import PluginManager from "@tokenring-ai/app/PluginManager";
import type { TokenRingPlugin } from "@tokenring-ai/app";

const pluginManager = new PluginManager(app);

const myPlugin: TokenRingPlugin = {
name: "MyPlugin",
version: "1.0.0",
description: "Custom plugin",
install(app) {
// Set up plugin (cannot await)
},
start(app) {
// Start plugin (can await)
}
};

await pluginManager.installPlugins([myPlugin]);

// Get all installed plugins
const plugins = pluginManager.getPlugins();

Background Task Management

import TokenRingApp, { TokenRingService } from "@tokenring-ai/app";

class BackgroundService implements TokenRingService {
name = "BackgroundService";
description = "Runs background tasks";

run(signal: AbortSignal) {
// Run a background task that will be tracked
app.runBackgroundTask(this, async (signal) => {
while (!signal.aborted) {
const result = await longRunningOperation();
processResult(result);
}
});
}
}

const app = new TokenRingApp(config);
app.addServices(new BackgroundService());

// Errors in background tasks are automatically logged

Service Auto-Restart

import TokenRingApp, { TokenRingService } from "@tokenring-ai/app";

class UnstableService implements TokenRingService {
name = "UnstableService";
description = "Service that may exit unexpectedly";

async run(signal: AbortSignal) {
// If this exits without error, it will restart after 5 seconds
// If this throws an error, it will restart after 5 seconds
// Services continue restarting until shutdown is called
await doWork();
}
}

const app = new TokenRingApp(config);
app.addServices(new UnstableService());

// The service will automatically restart if it exits unexpectedly
await app.run();

Service Auto-Restart Behavior

When app.run() is called, the application:

  1. Calls start() on all registered services
  2. Runs run() on all services that have it in a loop:
    • If a service exits unexpectedly, it logs an error and restarts after 5 seconds
    • If a service throws an error, it logs the error and restarts after 5 seconds
    • Services continue running until the abort signal is triggered
  3. Calls stop() on all registered services when shutdown

State Checkpointing

import fs from "node:fs";

// Generate checkpoint
const checkpoint = app.generateStateCheckpoint();
console.log(checkpoint.sessionId);
console.log(checkpoint.createdAt);

// Save checkpoint
await fs.writeFile("checkpoint.json", JSON.stringify(checkpoint));

// Restore checkpoint
const savedCheckpoint = JSON.parse(await fs.readFile("checkpoint.json", "utf-8"));
app.restoreState(savedCheckpoint.state);

Shutdown with Progress Indicator

import TokenRingApp, { TokenRingService } from "@tokenring-ai/app";

class LongRunningService implements TokenRingService {
name = "LongRunningService";
description = "Service that runs for a while";

async run(signal: AbortSignal) {
while (!signal.aborted) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}

const app = new TokenRingApp(config);
app.addServices(new LongRunningService());

// Start the app
const runPromise = app.run();

// Later, shutdown the app
app.shutdown("User requested shutdown");

// The shutdown will show a progress indicator
// and automatically exit when all services have stopped

AppLogsState Usage

import TokenRingApp from "@tokenring-ai/app";
import { AppLogsState } from "@tokenring-ai/app/state/AppLogsState";

const app = new TokenRingApp(config);

// Get logs
const logs = app.logs;
console.log(logs); // Array of LogEntry

// Access AppLogsState directly
const logsState = app.stateManager.getState(AppLogsState);
logsState.addLog("info", "Custom log message");
logsState.clear();

// Subscribe to log changes
const unsubscribe = app.stateManager.subscribe(AppLogsState, (state) => {
console.log("Logs updated:", state.getLogs());
});

Core Properties

TokenRingApp Properties

PropertyTypeDescription
configTokenRingAppConfigThe application configuration
logsLogEntry[]Array of logged system messages
sessionIdstringUnique human-readable session ID for this instance (generated via generateHumanId)
stateManagerStateManager<AppStateSlice<any>>State manager for app-level state
runningServicesSet<TokenRingService>Set of services currently running
stoppingServicesSet<TokenRingService>Set of services currently stopping
backgroundTasksMap<TokenRingService, number>Map tracking background task counts per service
servicesTypedRegistry<TokenRingService>Registry of all registered services
isShuttingDownbooleanWhether the app is in shutdown process

PluginManager Properties

PropertyTypeDescription
namestringAlways "PluginManager"
descriptionstringAlways "Manages plugins"

StateManager Properties

PropertyTypeDescription
stateMap<Function, SpecificStateSliceType>Internal state storage keyed by class constructor
startingStateRecord<string, unknown>Initial state for restoration

AppLogsState Properties

PropertyTypeDescription
logsLogEntry[]Array of log entries

Core Methods

TokenRingApp Methods

Service Management

addServices(...services: TokenRingService[]): void

Register services with the application. Services are automatically initialized in registration order.

requireService<T>(serviceType: abstract new (...args: any[]) => T): T

Get a service by type. Throws an error if the service is not found.

getService<T>(serviceType: abstract new (...args: any[]) => T): T | undefined

Get a service by type. Returns undefined if the service is not found.

getServices(): TokenRingService[]

Get all registered services.

waitForService<T extends TokenRingService>(
serviceType: abstract new (...args: any[]) => T,
callback: (service: T) => void
): void

Wait for a service to become available. The callback is invoked when the service is registered.

Logging

serviceOutput(service: TokenRingService, ...messages: any[]): void

Log system messages with formatted output. Messages are prefixed with the service name and stored in the logs array.

serviceError(service: TokenRingService, ...messages: any[]): void

Log error messages with formatted output. Messages are prefixed with the service name and logged at error level.

Background Task Management

runBackgroundTask(service: TokenRingService, initiator: (signal: AbortSignal) => Promise<void>): void

Track an app-level promise and log any errors that occur. The task runs in the background and errors are automatically logged to the service. Tracks the number of concurrent background tasks per service.

Configuration

getConfigSlice<T extends { parse: (any: any) => any }>(key: string, schema: T): z.output<T>

Get a validated config slice using a Zod schema. Throws if the key doesn't exist or validation fails.

State Management

generateStateCheckpoint(): AppSessionCheckpoint

Generate a session checkpoint containing the current state, session ID, timestamp, hostname, and project directory.

restoreState(state: Record<string, object>): void

Restore state from a checkpoint. Unknown state slices trigger a log message.

Lifecycle

shutdown(reason: string = "App shutdown for unknown reason"): void

Stop the application by aborting the internal AbortController. Displays a progress indicator showing services still running and background tasks. Automatically exits when all services have stopped.

async run(): Promise<void>

Start all registered services and run the application lifecycle. Returns a promise that resolves when all services complete or the abort signal is triggered.

PluginManager Methods

getPlugins(): TokenRingPlugin<ZodObject>[]

Get all installed plugins.

async installPlugins(plugins: TokenRingPlugin<any>[]): Promise<void>

Install plugins with configuration validation. The process is:

  1. Call install() on all plugins (if defined) - errors prevent plugin registration
  2. Register all plugins
  3. Call start() on all plugins (if defined) - errors prevent successful installation

Errors during installation prevent plugin registration. Errors during startup also prevent the plugin from being considered successfully installed.

For plugins with configuration, both install() and start() receive the parsed config as the second argument.

async reconfigurePlugins(newConfig: TokenRingAppConfig): Promise<{ restartRequired: boolean }>

Reconfigure all plugins with new application configuration. The process is:

  1. For each plugin with configuration, compare the current config slice with the new config slice
  2. If the config changed and the plugin has a reconfigure() method, call it
  3. If the config changed but the plugin doesn't support reconfiguration, set restartRequired to true

Returns { restartRequired: boolean } indicating whether any plugins require a restart.

StateManager Methods

initializeState<S, T extends SerializableStateSlice>(
StateClass: new (props: S) => T,
props: S
): void

Initialize a state slice with the given class and props. If the starting state contains data for this slice, it will be deserialized automatically.

getState<T extends SerializableStateSlice>(
StateClass: new (...args: any[]) => T
): T

Get a state slice by class. Throws if not initialized.

mutateState<R, T extends SerializableStateSlice>(
StateClass: new (...args: any[]) => T,
callback: (state: T) => R
): R

Mutate state with a callback. Returns the callback result. Automatically notifies all subscribers after mutation.

serialize(): Record<string, object>

Serialize all state slices to a record keyed by state slice class constructor.

deserialize(data: Record<string, unknown>, onMissing?: (key: string) => void): void

Deserialize state slices. Unknown keys trigger the onMissing callback. Validates data against serialization schema.

forEach(cb: (item: SpecificStateSliceType) => void): void

Iterate over all state slices.

slices(): IterableIterator<SpecificStateSliceType>

Get an iterator of all state slices.

subscribe<T extends SerializableStateSlice>(
StateClass: new (...args: any[]) => T,
callback: (state: T) => void
): () => void

Subscribe to state changes. Returns an unsubscribe function. The callback is invoked immediately via queueMicrotask with the current state.

waitForState<T extends SerializableStateSlice>(
StateClass: new (...args: any[]) => T,
predicate: (state: T) => boolean
): Promise<T>

Wait for a state predicate to become true. Returns a promise that resolves when the predicate is satisfied.

timedWaitForState<T extends SerializableStateSlice>(
StateClass: new (...args: any[]) => T,
predicate: (state: T) => boolean,
timeoutMs: number
): Promise<T>

Wait for a state predicate with timeout. Rejects with an error if the timeout is exceeded.

subscribeAsync<T extends SerializableStateSlice>(
StateClass: new (...args: any[]) => T,
signal: AbortSignal
): AsyncGenerator<T, void, unknown>

Async generator that yields state updates until aborted. Buffers state updates and yields them one at a time.

buildTokenRingAppConfig

async function buildTokenRingAppConfig<T extends z.ZodTypeAny>(
defaultConfig: z.input<T> & z.input<typeof TokenRingAppConfigSchema>
): Promise<z.output<T>>

Build application configuration by loading from multiple locations with Zod validation.

ParameterTypeDescription
defaultConfigz.input<T> & z.input<typeof TokenRingAppConfigSchema>Default configuration including app config structure

Config Loading Order: Config files are loaded from ~ (home) and dataDirectory in that order, with extensions .ts, .mjs, .cjs, .js. The config is validated at each step to ensure it is complete and well-formed.

Additional Behavior:

  • Creates the data directory if it doesn't exist
  • Creates a .gitignore file in the data directory if it doesn't exist (with *.sqlite* pattern)
  • Merges configs using deepMerge from @tokenring-ai/utility
  • Validates the merged config at each step using configSchema.parse()

API Reference

TokenRingApp

MethodDescription
addServices(...services)Register services with application
requireService(serviceType)Get service by type (throws if not found)
getService(serviceType)Get service by type (returns undefined if not found)
getServices()Get all registered services
waitForService(serviceType, callback)Wait for service to be available
getConfigSlice(key, schema)Get validated config slice
serviceOutput(service, ...messages)Log system messages with service context
serviceError(service, ...messages)Log error messages with service context
runBackgroundTask(service, initiator)Track background task and log errors
generateStateCheckpoint()Generate session checkpoint
restoreState(state)Restore state from checkpoint
shutdown(reason?)Stop the application with optional reason
run()Start application services and run lifecycle

PluginManager

MethodDescription
installPlugins(plugins)Install plugins with validation
getPlugins()Get all installed plugins
reconfigurePlugins(newConfig)Reconfigure all plugins

StateManager

MethodDescription
initializeState(StateClass, props)Initialize state slice
getState(StateClass)Get state slice
mutateState(StateClass, callback)Mutate state with callback
serialize()Serialize all state slices
deserialize(data, onMissing)Deserialize state slices
forEach(cb)Iterate over state slices
slices()Get iterator of state slices
subscribe(StateClass, callback)Subscribe to changes
waitForState(StateClass, predicate)Wait for state predicate
timedWaitForState(StateClass, predicate, timeout)Wait with timeout
subscribeAsync(StateClass, signal)Async state generator

buildTokenRingAppConfig

ParameterDescription
defaultConfigDefault configuration including app config structure

Types

TokenRingService

Interface for services that can be registered with the application.

interface TokenRingService {
readonly name: string;
description: string;
run?(signal: AbortSignal): Promise<void>;
start?(signal: AbortSignal): Promise<void> | void;
stop?(): Promise<void> | void;
attach?(agent: Agent, creationContext: AgentCreationContext): void;
detach?(agent: Agent): void;
}
Property/MethodDescription
nameUnique service name
descriptionHuman-readable service description
runMain service loop. Called after start(). Exited services are automatically restarted after 5 seconds
startInitialization logic. Called before run()
stopCleanup logic. Called during shutdown
attachAttach to an agent with creation context
detachDetach from an agent

TokenRingPlugin

There are two types of plugins:

Simple Plugin (no config)

{
readonly name: string;
version: string;
description: string;
install?: (app: TokenRingApp) => void; // Install does not allow awaiting, anything awaited must be done in start
start?: (app: TokenRingApp) => Promise<void> | void;
}

Plugin with Configuration

{
readonly name: string;
version: string;
description: string;
config: ConfigType; // Zod schema
install?: (app: TokenRingApp, config: z.output<ConfigType>) => void;
start?: (app: TokenRingApp, config: z.output<ConfigType>) => Promise<void> | void;
reconfigure?: (app: TokenRingApp, config: z.output<ConfigType>) => Promise<void> | void;
}

Important Notes:

  • install() cannot be awaited. Any async operations must be done in start()
  • start() is called after all plugins are installed
  • reconfigure() is called when plugin configuration changes and the plugin supports reconfiguration
  • For plugins with config, both install() and start() receive the parsed config

SerializableStateSlice

Abstract base class for state slices that can be serialized and deserialized.

abstract class SerializableStateSlice<SerializationSchema extends z.ZodTypeAny> {
constructor(
public readonly name: string,
public readonly serializationSchema: SerializationSchema
)
abstract serialize(): z.input<SerializationSchema>;
abstract deserialize(data: z.output<SerializationSchema>): void;

getValidatedState(stateSnapshot: StateSnapshot): z.output<SerializationSchema> | null;
}
MethodDescription
serialize()Serialize state to a format matching the schema's input
deserialize()Deserialize state from validated schema output
getValidatedState()Get validated state from a state snapshot, returns null if not present

AppStateSlice

Abstract base class for app state slices.

abstract class AppStateSlice<SerializationSchema extends z.ZodTypeAny>
extends SerializableStateSlice<SerializationSchema>

AppSessionCheckpoint

Interface for app session checkpoints.

interface AppSessionCheckpoint {
sessionId: string;
createdAt: number;
hostname: string;
projectDirectory: string;
state: Record<string, object>;
}
PropertyDescription
sessionIdUnique session ID
createdAtTimestamp when checkpoint was created
hostnameHostname of the machine
projectDirectoryProject directory path
stateSerialized state

StateStorageInterface

Interface for state storage implementations.

interface StateStorageInterface<SpecificStateSliceType extends SerializableStateSlice<any>> {
getState<T extends SpecificStateSliceType>(ClassType: new (...args: any[]) => T): T;
mutateState<R, T extends SpecificStateSliceType>(
ClassType: new (...args: any[]) => T,
callback: (state: T) => R,
): R;
initializeState<S, T extends SpecificStateSliceType>(
ClassType: new (props: S) => T,
props: S,
): void;
}

TokenRingAppConfig

export const TokenRingAppConfigSchema = z.object({
app: z.object({
dataDirectory: z.string(),
configFileName: z.string(),
configSchema: z.custom<z.ZodTypeAny>(),
shutdownMonitorIntervalMs: z.number().default(2000),
serviceRestartDelayMs: z.number().default(5000),
})
});

export const LooseTokenRingAppConfigSchema = TokenRingAppConfigSchema.loose();
export type TokenRingAppConfig = z.output<typeof LooseTokenRingAppConfigSchema>;

LogEntry

type LogEntry = {
timestamp: number;
level: "info" | "error";
message: string;
};

AppLogsState

State slice for managing application logs.

export class AppLogsState extends AppStateSlice<typeof serializationSchema> {
logs: LogEntry[] = [];

constructor() {
super("AppLogsState", serializationSchema);
}

serialize(): z.output<typeof serializationSchema> {
return { logs: this.logs };
}

deserialize(data: z.output<typeof serializationSchema>): void {
this.logs = data.logs ?? [];
}

addLog(level: "info" | "error", message: string): void {
this.logs.push({ timestamp: Date.now(), level, message });
}

clear(): void {
this.logs = [];
}

getLogs(): LogEntry[] {
return this.logs;
}
}
MethodDescription
addLog(level, message)Add a log entry with timestamp
clear()Clear all logs
getLogs()Get all log entries

Error Handling

The application provides comprehensive error handling:

  • Configuration Errors: Zod validation errors with descriptive messages when config is invalid
  • Service Not Found: Clear error when requiring a service that doesn't exist via requireService()
  • Promise Errors: Automatic logging of errors in background tasks via runBackgroundTask()
  • Lifecycle Errors: Graceful shutdown handling during startup failures; services that exit unexpectedly are restarted after 5 seconds
  • State Errors: Safe deserialization with error callbacks for unknown keys; schema validation on deserialize
  • Plugin Errors: Errors during plugin installation prevent plugin registration; errors during startup also prevent successful installation

Best Practices

Service Lifecycle Management

  • Implement start() for initialization that can be awaited
  • Implement run() for long-running loops that check the abort signal
  • Implement stop() for cleanup logic
  • Always check signal.aborted in long-running loops

Plugin Development

  • Use install() for synchronous setup only
  • Use start() for async initialization
  • Implement reconfigure() if your plugin supports dynamic configuration
  • Validate configuration using Zod schemas

State Management

  • Use serialize() and deserialize() for persistence
  • Implement subscribe() for reactive updates
  • Use waitForState() for conditional logic based on state changes
  • Use subscribeAsync() for streaming state updates

Error Handling

  • Use serviceOutput() for informational messages
  • Use serviceError() for error messages
  • Handle abort signals in all long-running operations
  • Implement proper cleanup in stop() methods

Testing

Testing Setup

bun test
bun test:watch
bun test:coverage

Package Structure

pkg/app/
├── TokenRingApp.ts # Main application class
├── PluginManager.ts # Plugin lifecycle management
├── StateManager.ts # State management utility
├── buildTokenRingAppConfig.ts # Config builder function
├── types.ts # Type definitions
├── index.ts # Main exports
├── state/
│ └── AppLogsState.ts # Application logs state slice
├── test/
│ ├── TokenRingApp.test.ts
│ ├── PluginManager.test.ts
│ ├── StateManager.test.ts
│ ├── integration.test.ts
│ └── createTestingApp.ts
└── LICENSE

Development Guidelines

  • Follow established coding patterns
  • Write unit tests for new functionality using vitest
  • Ensure Zod schema validation for all configuration
  • Update documentation for new features
  • Test with multiple service configurations
  • Verify service lifecycle (start, run, stop)
  • Test error handling and auto-restart behavior
  • Verify state serialization and deserialization

Dependencies

  • @tokenring-ai/agent: 0.2.0
  • @tokenring-ai/utility: 0.2.0
  • uuid: ^13.0.0
  • zod: ^4.3.6

Dev Dependencies

  • vitest: ^4.1.0
  • typescript: ^5.9.3

License

MIT License - see LICENSE file for details.

Copyright (c) 2025 Mark Dierolf