Skip to main content

App Plugin

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
  • State Isolation: Separate state slices with serialization and deserialization support
  • Signal-Based Shutdown: Graceful termination using AbortSignal
  • Promise Tracking: Automatic error handling for async operations
  • Scheduled Tasks: Built-in task scheduling for recurring operations
  • Comprehensive Logging: Structured output for system messages and errors
  • Async State Subscriptions: Support for async state observation with abort handling

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.

Usage Examples

Basic Application Setup

import TokenRingApp from "@tokenring-ai/app";

const app = new TokenRingApp("/path/to/app", {
apiKey: process.env.API_KEY,
model: "gpt-4"
});

Service Management

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

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

async run(signal: AbortSignal) {
console.log("MyService started");
signal.addEventListener("abort", () => {
console.log("MyService stopped");
});
}

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

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

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

Plugin with Configuration

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

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}`);
},
start(app, config) {
console.log(`Starting with model: ${config.model}`);
},
reconfigure(app, config) {
console.log(`Reconfiguring with model: ${config.model}`);
}
};

await pluginManager.installPlugins([myPlugin]);

State Management

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

interface UserState extends SerializableStateSlice {
name: string;
email: string;
}

class UserStateSlice implements UserState {
name = "UserState";
name: string;
email: string;

constructor(props: { name: string; email: string }) {
this.name = props.name;
this.email = props.email;
}

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

deserialize(data: object) {
this.name = (data as UserState).name;
this.email = (data as UserState).email;
}
}

// Initialize state
const stateManager = new StateManager<UserState>();
stateManager.initializeState(
UserStateSlice,
new 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 stateStream = stateManager.subscribeAsync(UserStateSlice, signal);
for await (const state of stateStream) {
console.log("New state:", state);
}

Scheduled Tasks

// Schedule a task that runs every 5 seconds
app.scheduleEvery(5000, async () => {
const result = await fetchData();
console.log("Scheduled task result:", result);
});

// The task can be stopped by shutting down the app
app.shutdown();

Build Config from Files

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

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

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

Plugin Configuration

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
},
start(app) {
// Start plugin
}
};

await pluginManager.installPlugins([myPlugin]);

Plugin Reconfiguration

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

app.addServices(myPlugin);

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

Core Properties

TokenRingApp Properties

PropertyTypeDescription
configTokenRingAppConfigThe application configuration
packageDirectorystringPath to the application directory
logsLogEntry[]Array of logged system messages
servicesTypedRegistry<TokenRingService>Registry of all registered services

PluginManager Properties

PropertyTypeDescription
namestringAlways "PluginManager"
descriptionstringAlways "Manages plugins"

StateManager Properties

PropertyTypeDescription
stateMap<string, SpecificStateSliceType>Internal state storage

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>(
serviceType: abstract new (...args: any[]) => T,
callback: (service: R) => void
): void

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

Logging

serviceOutput(...messages: any[]): void

Log system messages with formatted output.

serviceError(...messages: any[]): void

Log error messages with formatted output.

Promise Management

trackPromise(initiator: (signal: AbortSignal) => Promise<void>): void

Track an app-level promise and log any errors that occur.

Scheduling

scheduleEvery(
interval: number,
callback: () => Promise<void>,
signal?: AbortSignal
): void

Schedule a recurring task with a specified interval. The task runs until aborted.

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.

Lifecycle

shutdown(): void

Stop the application by aborting the internal AbortController.

run(): Promise<void>

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

PluginManager Methods

getPlugins(): TokenRingPlugin<unknown>[]

Get all installed plugins.

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

Install plugins with configuration validation. All plugins are installed first, then started. Errors during installation prevent plugin registration.

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

Reconfigure all plugins with new application configuration. Plugins with reconfigure methods are updated if their config changed. Plugins without reconfiguration support 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.

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.

serialize(): Record<string, object>

Serialize all state slices to a record.

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

Deserialize state slices. Unknown keys trigger the onMissing callback.

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

Iterate over all state slices.

entries(): IterableIterator<[string, SerializableStateSlice]>

Get an iterator of [key, value] pairs for 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.

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

Wait for a state predicate to become true.

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

Wait for a state predicate with timeout.

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

Async generator that yields state updates until aborted.

buildTokenRingAppConfig

async function buildTokenRingAppConfig<ConfigSchema extends ZodObject>({
workingDirectory,
dataDirectory,
configFileName,
configSchema,
defaultConfig,
mergeConfig
}: CreateTokenRingAppOptions<ConfigSchema>): Promise<z.output<ConfigSchema>>

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

ParameterTypeDescription
workingDirectorystringPath to source/working directory
dataDirectorystringPath to data directory
configFileNamestringBase name of config files (without extension)
configSchemaConfigSchemaZod schema for validation
defaultConfigz.input<ConfigSchema>Default configuration
mergeConfigfunctionOptional config merge function (default: deepMerge)

Config Loading Order: Config files are loaded from ~ (home) and dataDirectory in that order, with extensions .ts, .mjs, .cjs, .js.

Configuration

Application Configuration Schema

const TokenRingAppConfigSchema = z.record(z.string(), z.unknown());
type TokenRingAppConfig = z.infer<typeof TokenRingAppConfigSchema>;

Plugin Configuration Schema

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
}
}
};

Config Loading Order

Config files are loaded from ~ (home) and dataDirectory in that order, with extensions .ts, .mjs, .cjs, .js.

Integration

Plugin Integration

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

const pluginManager = new PluginManager(app);

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

await pluginManager.installPlugins([myPlugin]);

State Serialization

// Save state
const serialized = stateManager.serialize();
await fs.writeFile("state.json", JSON.stringify(serialized));

// Load state
const data = JSON.parse(await fs.readFile("state.json", "utf-8"));
stateManager.deserialize(data, (key) => {
console.log(`Unknown state: ${key}`);
});

Abort Signal Handling

const app = new TokenRingApp("/path", {});

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

// Stop the app
app.shutdown();

Plugin Reconfiguration

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
}
};

app.addServices(myPlugin);

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

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(...messages)Log system messages
serviceError(...messages)Log error messages
trackPromise(initiator)Track promise and log errors
scheduleEvery(interval, callback, signal)Schedule recurring task
shutdown()Stop the application
run()Start application services

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
entries()Get [key, value] iterator
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

Types

TokenRingService

interface TokenRingService {
name: string;
description: string;
run?(signal: AbortSignal): Promise<void> | void;
attach?(agent: Agent): void;
detach?(agent: Agent): void;
// Legacy methods - set to never type to cause tsc to flag the use of these methods
install?: never;
start?: never;
stop?: never;
getContextItems?: never;
}

TokenRingPlugin

type TokenRingPlugin<ConfigType> = {
name: string;
version: string;
description: string;
install?: (app: TokenRingApp) => void | undefined; // Install does not allow awaiting, anything awaited must be done in start
start?: (app: TokenRingApp) => Promise<void> | void;
} | {
name: string;
version: string;
description: string;
config: ConfigType;
install?: (app: TokenRingApp, config: z.output<ConfigType>) => void | undefined; // Install does not allow awaiting, anything awaited must be done in start
start?: (app: TokenRingApp, config: z.output<ConfigType>) => Promise<void> | void;
reconfigure?: (app: TokenRingApp, config: z.output<ConfigType>) => Promise<void> | void;
};

SerializableStateSlice

interface SerializableStateSlice {
name: string;
serialize(): object;
deserialize(data: object): void;
}

StateStorageInterface

interface StateStorageInterface<T 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.record(z.string(), z.unknown());
export type TokenRingAppConfig = z.infer<typeof TokenRingAppConfigSchema>;

LogEntry

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

Error Handling

The application provides comprehensive error handling:

  • Configuration Errors: Zod validation errors with descriptive messages
  • Service Not Found: Clear error when requiring a service that doesn't exist
  • Promise Errors: Automatic logging of unhandled promise rejections
  • Lifecycle Errors: Graceful shutdown handling during startup failures
  • State Errors: Safe deserialization with error callbacks
  • Plugin Errors: Errors during plugin installation prevent plugin registration

Development

Testing

bun test
bun test:watch
bun test:coverage

Building

bun run build

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
├── test/
│ ├── TokenRingApp.test.ts
│ ├── PluginManager.test.ts
│ ├── StateManager.test.ts
│ ├── integration.test.ts
│ └── createTestingApp.ts
└── LICENSE

Contribution Guidelines

  • Follow established coding patterns
  • Write unit tests for new functionality
  • Ensure Zod schema validation for all configuration
  • Update documentation for new features
  • Test with multiple service configurations

License

MIT License - see LICENSE for details.