Skip to content

@chamvi/app ​

Boots chamvi agents as an app. The public API of packages/app/src/index.ts and its subpath entries; see Apps & Modules for the concepts.

bash
npm install @chamvi/app ai typeorm pg

ai, typeorm and pg are peer dependencies. The provider packages (@ai-sdk/anthropic, @ai-sdk/openai, @ai-sdk/google, @ai-sdk/mistral, @ai-sdk/xai) and the @authup/* packages behind @chamvi/app/authup are optional peers. The other optional modules ship with their feature packages (@chamvi/github/module, ...), so @chamvi/app depends on none of them. The main entry re-exports @chamvi/core and agentPermission, Permission and the Authenticator, Principal, HttpRoute, HttpRouteContext, HttpRouteRequest, HttpRouteResult and StaticToken types of @chamvi/http.

EntryExportsNeeds
@chamvi/appcreateApp, isApp, the built-in modules, the module contract (tokens, ModuleName, AppContext, ILifecycleHook, the IModule and IContainer types), env readers, approvalUrlOf, readConfig, createLogger, createModelRegistry
@chamvi/app/authupauthupModule, oidcModule@authup/server-adapter-kit, @authup/core-http-kit
@chamvi/app/testingbuildModuleHarness, createModuleHarness

createApp ​

ts
function createApp(options: AppOptions): IApp;
function isApp(value: unknown): value is IApp;

const APP_BRAND: unique symbol; // Symbol.for('chamvi.app')

Synchronous and side-effect free. Throws InvalidAgentDefinitionError for a module list entry that is no IModule and for two modules of one name (a built-in's name included: list one module to replace a built-in). isApp checks the brand, so an app of another installed copy matches.

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { Agent } from '@chamvi/core';
import type { IModule } from 'orkos';
import type { Env } from '../env/types.ts';
import type { ModelProviders } from '../models/types.ts';
import type { APP_BRAND } from './constants.ts';

export type AppOptions = {
    agents: Agent[];
    /** Extra modules; one named like a built-in (`ModuleName`) replaces it. */
    modules?: IModule[];
    /** Providers for string models, passed to the AI SDK's createProviderRegistry. */
    models?: ModelProviders;
};

export type AppLifecycleOptions = {
    /** Default `process.env`. */
    env?: Env;
};

export type AppStartOptions = AppLifecycleOptions & {
    dev?: boolean;
};

export type AppRunJobOptions = AppLifecycleOptions & {
    agent: string;
    /** Cron trigger id; optional when the agent has exactly one cron trigger. */
    trigger?: string;
};

export type AppRunJobResult = {
    runId?: string;
    status: string;
    exitCode: 0 | 1;
};

/**
 * An app: built by createApp, run once by one of `start`, `runJob` or
 * `migrate` (a second lifecycle rejects with `app_in_use`).
 */
export interface IApp {
    readonly [APP_BRAND]: true;
    readonly agents: readonly Agent[];
    /** Sets every module up; resolves once the HTTP server listens. */
    start(options?: AppStartOptions): Promise<void>;
    /**
     * Memoized: aborts the lifecycle, waits for it, tears every module down;
     * rejects with `app_stop_failed` when anything was reported meanwhile.
     */
    stop(): Promise<void>;
    /** Fires one cron trigger once and waits for its run (no scheduler, no HTTP). */
    runJob(options: AppRunJobOptions): Promise<AppRunJobResult>;
    /** Migrates the store (DATABASE_URL required) and closes it. */
    migrate(options?: AppLifecycleOptions): Promise<void>;
}
MethodBuildsRejects with
startconfig, store (migrated unless CHAMVI_MIGRATE=false), engine, scheduler (unless CHAMVI_SCHEDULER=false), http; starts the engine and scheduler once every module built, resolves once listeningthe setup error (a module graph error as InvalidAgentDefinitionError); app_stopped when stop() interrupted the setup (the setup's own error is logged); app_in_use for a second lifecycle or after stop()
stopthe shutdown sequence (see Start and stop); memoized, waits for a setup in flightapp_stop_failed when a teardown failed (each failure is logged)
runJobconfig, store (migrated), engine (not started: @chamvi/scheduler runJob starts, ticks and drains it); no scheduler, no httpthe job's error; app_in_use
migrateconfig, store (migrateStore), store teardownInvalidAgentDefinitionError without DATABASE_URL; app_in_use

runJob resolves exitCode: 0 for a completed, waiting, skipped, queued-behind-an-approval or signal-interrupted run, 1 for failed or cancelled.

Errors ​

ChamviError codes in APP_ERROR_CODE:

CodeWhen
app_stoppedThe lifecycle's abort reason once stop() was called; a start stopped while setting up rejects with it
app_in_useA second start, runJob or migrate on the same app
app_stop_failedstop() (or a job's teardown) after one or more teardown errors, each logged

Configuration errors (a malformed or missing variable, a model string without its provider, a duplicate module name) are core InvalidAgentDefinitionErrors; they name the variable and never echo its value.

Environment ​

Read by readConfig(env, { mode, dev }) once per lifecycle; the optional module factories read their own variables when they are called. Empty string is unset, integers are /^\d+$/ (every *_MS variable at most TIMER_MAX_MS, 2147483647, since a larger Node timer fires after 1 ms), booleans true, false, 1 or 0.

VariableModuleTypeDefaultRequired when
PORThttpint 1..655353000never
HOSThttpstring0.0.0.0never
CHAMVI_BASE_PATHhttpempty or ^(?:\/[a-z0-9._~-]+)+$'' (health at /healthz, UI at /ui)never
CHAMVI_PUBLIC_URLhttp, ui, notifiersabsolute URLunsetan approval link is wanted
CHAMVI_LOG_LEVELconfigerror, warn, info, debuginfonever
DATABASE_URLstorepostgres://...unset: MemoryStore (warned outside dev)migrate; any multi-replica or CronJob deployment
CHAMVI_MIGRATEstorebooltruenever
CHAMVI_PURGE_INTERVAL_MSstoreint, 0 disables3600000 (only with TypeormStore, on engine.clock)never
RESUME_SECRETenginestring, at least 32 charactersnone (dev: random per process, warned)start, run-job
CHAMVI_MAX_CONCURRENT_RUNSengineint >= 14never
CHAMVI_SCHEDULERschedulerbooltrue; false runs no in-process cron (CronJob mode)never
CHAMVI_READINESS_DELAY_MShttp shutdownint0never
CHAMVI_DRAIN_TIMEOUT_MShttp shutdownint30000 (dev 5000)never
CHAMVI_CLOSE_TIMEOUT_MShttp shutdownint5000never
CHAMVI_FORCE_EXIT_MScliint90000 (dev 15000)never
CHAMVI_TOKENSstatic tokensJSON [{ "value", "id", "permissions": [] }], value at least 16 characters and uniqueunset (dev: one random * token, warned)no other authenticator is configured, outside dev
AUTHUP_URL, AUTHUP_CLIENT_ID, AUTHUP_CLIENT_SECRETauthupModuleURL, string, stringunsetauthupModule() without options
CHAMVI_UI_AUTHUP_CLIENT_ID, CHAMVI_UI_AUTHUP_REALM_IDuiModulestringunset (token login only)the dashboard signs in through authup (URL from AUTHUP_URL)
ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, MISTRAL_API_KEY, XAI_API_KEYmodelsstringunseta model string names that provider and models does not override it; ANTHROPIC_BASE_URL, OPENAI_BASE_URL are passed on
GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY (PEM, literal \n allowed), GITHUB_WEBHOOK_SECRET (comma-separated for rotation)githubModulestringnonegithubModule() without options
GITHUB_INSTALLATION_ID, GITHUB_APP_SLUG, GITHUB_API_URLgithubModuleint, slug, https URLunsetnever
OPENCLAW_BASE_URL, OPENCLAW_HOOKS_TOKEN, OPENCLAW_CHANNEL, OPENCLAW_TOopenclawModuleURL, string, string, stringnoneopenclawModule() without options
OPENCLAW_HOOKS_PATH, OPENCLAW_AGENT_ID, OPENCLAW_ACCOUNT_IDopenclawModulestring/hooks, unset, unsetnever
CHAMVI_SANDBOX_PROVIDERsandboxModulelocal, docker, kubernetesthe provider option, else localnever
CHAMVI_SANDBOX_NAMESPACEsandboxModulestringnoneprovider kubernetes
CHAMVI_SANDBOX_IMAGEsandboxModuleimage refghcr.io/chamvi/sandbox-node:24never
ts
function readConfig(env: Env, options: ReadConfigOptions): AppConfig;
function approvalUrlOf(config: AppConfig): ((ref: ApprovalRef) => string) | undefined; // needs CHAMVI_PUBLIC_URL

The env readers modules build on, each throwing InvalidAgentDefinitionError that names the variable:

ts
function envString(env: Env, name: string, options?: EnvStringOptions): string | undefined;
function envInteger(env: Env, name: string, options?: EnvIntegerOptions): number | undefined;
function envBoolean(env: Env, name: string, options?: EnvReadOptions<boolean>): boolean | undefined;
function envUrl(env: Env, name: string, options?: EnvUrlOptions): string | undefined;
function envList(env: Env, name: string, options?: EnvReadOptions<string[]>): string[] | undefined; // comma-separated
function envJson(env: Env, name: string, options?: EnvJsonOptions): unknown;
function envOneOf<T extends string>(env: Env, name: string, values: readonly T[], options?: EnvReadOptions<T>): T | undefined;

With required: true or a default the overloads return a defined value.

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

export type Env = Record<string, string | undefined>;

export type EnvReadOptions<T> = {
    /** Used when the variable is unset or empty. */
    default?: T;
    /** Throws when the variable is unset or empty and no default is given. */
    required?: boolean;
};

/** Options that make a reader return a defined value. */
export type EnvDefinedOptions<T> = { required: true } | { default: T };

export type EnvStringOptions = EnvReadOptions<string> & { minLength?: number };

export type EnvIntegerOptions = EnvReadOptions<number> & { min?: number; max?: number };

export type EnvUrlOptions = EnvReadOptions<string> & { protocols?: readonly string[] };

export type EnvJsonOptions = { required?: boolean };

Built-in modules ​

ts
function configModule(options?: ConfigModuleOptions): IModule;            // 'chamvi:config'
function storeModule(options?: StoreModuleOptions): IModule;              // 'chamvi:store'
function staticTokensModule(options?: StaticTokensModuleOptions): IModule; // 'chamvi:static-tokens'
function engineModule(options?: EngineModuleOptions): IModule;            // 'chamvi:engine'
function schedulerModule(options?: SchedulerModuleOptions): IModule;      // 'chamvi:scheduler'
function httpModule(options?: HttpModuleOptions): IModule;                // 'chamvi:http'
function consoleNotifierModule(options?: ConsoleNotifierModuleOptions): IModule; // 'chamvi:notifier:<name>'

Listing one of them in createApp({ modules }) replaces the default instance.

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { IClock } from '@chamvi/core';
import type { ILogger } from '../../logger/types.ts';

export type ConfigModuleOptions = {
    /** `AppToken.CLOCK`; default `systemClock`. */
    clock?: IClock;
    /** `AppToken.LOGGER`; default the JSON line logger at `CHAMVI_LOG_LEVEL`. */
    logger?: ILogger;
};

StoreModuleOptions (store is used as is and never closed; dataSource must be initialized, is migrated like a built one and never destroyed; url wins over DATABASE_URL; a data source built from a URL waits at most 10 s for Postgres to accept the first connection):

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { IClock, IStore } from '@chamvi/core';
import type { DataSource } from 'typeorm';
import type { ILogger } from '../../logger/types.ts';

export type StoreModuleOptions = {
    /** A Postgres URL; default `DATABASE_URL`. */
    url?: string;
    /** Used as is and never closed. */
    store?: IStore;
    /** An initialized data source, used for a TypeormStore and never destroyed. */
    dataSource?: DataSource;
};

/** A store with `TypeormStore`'s purge of expired keys and locks. */
export type PurgingStore = {
    purgeExpired(): Promise<unknown>;
};

export type PurgeTimerOptions = {
    store: PurgingStore;
    clock: IClock;
    intervalMs: number;
    logger: ILogger;
};

export interface IPurgeTimer {
    start(): void;
    /** Idempotent; keeps a call in flight from re-arming and resolves once it settled. */
    stop(): Promise<void>;
}
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { StaticToken } from '@chamvi/http';

export type StaticTokensModuleOptions = {
    /** Default `CHAMVI_TOKENS`. */
    tokens?: StaticToken[];
};
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

export type EngineModuleOptions = {
    /** Claim owner; default the engine's `${hostname}-${pid}`. */
    owner?: string;
    /** Claim lease; default the engine's 60 s. */
    claimTtlMs?: number;
};
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

export type SchedulerModuleOptions = {
    /** Names of the agents to schedule; default every agent. */
    agents?: string[];
    /** Lock owner; default the scheduler's `${hostname}-${pid}`. */
    owner?: string;
    /** Default the scheduler's SCHEDULE_LOCK_TTL_MS. */
    lockTtlMs?: number;
};
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { HttpAppOptions } from '@chamvi/http';

/**
 * `createHttpApp` options the module passes through; `engine`, `auth`,
 * `shutdown`, `routes`, `basePath` and `onError` come from the app.
 */
export type HttpModuleOptions = Omit<HttpAppOptions, 'engine' | 'auth' | 'shutdown' | 'routes' | 'basePath' | 'onError'>;

Optional modules ​

ts
// @chamvi/github/module
function githubModule(options?: GithubModuleOptions): GithubModule;        // .app: IGithubApp, 'chamvi:github'

// @chamvi/notify-github/module
function githubNotifierModule(options: GithubNotifierModuleOptions): IModule; // 'chamvi:notifier:<name>', default name 'github'

// @chamvi/sandbox/module
function sandboxModule(options?: SandboxModuleOptions): SandboxModule;     // .manager: IWorkspaceManager, 'chamvi:sandbox'

// @chamvi/notify-openclaw/module
function openclawModule(options?: OpenclawModuleOptions): IModule;         // 'chamvi:notifier:<name>', default name 'openclaw'

// @chamvi/ui/module
function uiModule(options?: UiModuleOptions): IModule;                     // 'chamvi:ui'

// @chamvi/app/authup
function authupModule(options?: AuthupModuleOptions): IModule;             // 'chamvi:authup'
function oidcModule(options: OidcOptions): IModule;                        // 'chamvi:oidc', no env form

Each factory reads options.env ?? process.env when it is called and throws InvalidAgentDefinitionError naming a missing or malformed variable. Options win over env, except CHAMVI_SANDBOX_PROVIDER, CHAMVI_SANDBOX_NAMESPACE and CHAMVI_SANDBOX_IMAGE, which win over the sandbox options. The notifier modules default approvalUrl to the dashboard link built from CHAMVI_PUBLIC_URL and use the app's clock. Each module but uiModule and the authup ones provides its instance on the engine in its engineCreated hook; tools reach it through the package's context pair (github, notify-github, sandbox, notify-openclaw).

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { Env } from '@chamvi/app';
import type { GithubAppOptions, IGithubApp } from '../app/types.ts';

export type GithubModuleOptions = Partial<GithubAppOptions> & {
    /** Used as is; no env is read then. */
    app?: IGithubApp;
    /** Default `process.env`, read when the factory is called. */
    env?: Env;
    /** Default `/webhooks/github`. */
    webhookPath?: string;
};
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { GithubNotifierOptions } from '../notifier/types.ts';

/** `githubNotifier()` options; `approvalUrl` defaults to the UI link from `CHAMVI_PUBLIC_URL`, `clock` is the app's. */
export type GithubNotifierModuleOptions = Omit<GithubNotifierOptions, 'clock'>;
ts
export type SandboxProviderName = 'local' | 'docker' | 'kubernetes';

/**
 * `createWorkspaceManager()` options; the manager's `env` is `workspaceEnv`
 * here, since `env` is the process env the factory reads.
 */
export type SandboxModuleOptions = Omit<WorkspaceManagerOptions, 'provider' | 'env'> & {
    /** A provider name (`CHAMVI_SANDBOX_PROVIDER` wins) or an instance; default `'local'`. */
    provider?: SandboxProviderName | ISandboxProvider;
    /** Used as is; no provider is built then. */
    manager?: IWorkspaceManager;
    /** Kubernetes namespace of the workspace pods; `CHAMVI_SANDBOX_NAMESPACE` wins. */
    namespace?: string;
    /** Docker and Kubernetes image; `CHAMVI_SANDBOX_IMAGE` wins, default `DEFAULT_SANDBOX_IMAGE`. */
    image?: string;
    /** Added to every workspace's environment. */
    workspaceEnv?: Record<string, string>;
    /** Default `process.env`, read when the factory is called. */
    env?: Env;
};
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { Env } from '@chamvi/app';
import type { OpenclawNotifierOptions } from '../notifier/types.ts';

/**
 * `openclaw()` options; the ones not given come from env, `approvalUrl`
 * defaults to the UI link from `CHAMVI_PUBLIC_URL`, `clock` to the app's.
 */
export type OpenclawModuleOptions = Partial<OpenclawNotifierOptions> & {
    /** Default `process.env`, read when the factory is called. */
    env?: Env;
};
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { Env } from '@chamvi/app';
import type { UiRoutesOptions } from '../routes/types.ts';

/** `uiRoutes()` options; `auth` defaults to the token login plus authup from env. */
export type UiModuleOptions = UiRoutesOptions & {
    /** Default `process.env`, read when the factory is called. */
    env?: Env;
};
ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { ITokenVerifier } from '@authup/server-adapter-kit';
import type { Env } from '../env/types.ts';

export type AuthupModuleOptions = {
    /** Used as is; no env is read then. */
    verifier?: ITokenVerifier;
    /** Default `AUTHUP_URL`. */
    url?: string;
    /** Default `AUTHUP_CLIENT_ID`. */
    clientId?: string;
    /** Default `AUTHUP_CLIENT_SECRET`. */
    clientSecret?: string;
    /** Default `process.env`, read when the factory is called. */
    env?: Env;
};

Tokens and hooks ​

AppToken holds the container keys, ModuleName the built-in names, AUTHENTICATOR_ORDER the chain order (static tokens 0, authup 10, oidc 20, lowest first):

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import { TypedToken } from 'eldin';
import type { AppTokens } from './types.ts';

function token<K extends keyof AppTokens>(key: K): TypedToken<AppTokens[K]> {
    return new TypedToken<AppTokens[K]>(`chamvi.${key.toLowerCase().replace(/_/g, '-')}`);
}

/**
 * The container keys modules share. `HTTP_ROUTES`, `AUTHENTICATORS`,
 * `NOTIFIERS` and `LIFECYCLE_HOOKS` are collections (`registerMany` in
 * `setup`, `resolveAll` in `onReady`); every other token holds one value.
 */
export const AppToken = {
    CONTEXT: token('CONTEXT'),
    CONFIG: token('CONFIG'),
    LOGGER: token('LOGGER'),
    CLOCK: token('CLOCK'),
    STORE: token('STORE'),
    DATA_SOURCE: token('DATA_SOURCE'),
    ENGINE: token('ENGINE'),
    SCHEDULER: token('SCHEDULER'),
    SHUTDOWN: token('SHUTDOWN'),
    SERVER: token('SERVER'),
    HTTP_ROUTES: token('HTTP_ROUTES'),
    AUTHENTICATORS: token('AUTHENTICATORS'),
    NOTIFIERS: token('NOTIFIERS'),
    LIFECYCLE_HOOKS: token('LIFECYCLE_HOOKS'),
} as const;

/** The built-in modules' names; list a module of the same name to replace one. */
export const ModuleName = {
    CONFIG: 'chamvi:config',
    STORE: 'chamvi:store',
    STATIC_TOKENS: 'chamvi:static-tokens',
    ENGINE: 'chamvi:engine',
    SCHEDULER: 'chamvi:scheduler',
    HTTP: 'chamvi:http',
} as const;

/** The order of the authenticator chain (lowest first). */
export const AUTHENTICATOR_ORDER = {
    STATIC_TOKENS: 0,
    AUTHUP: 10,
    OIDC: 20,
} as const;

export const APP_ERROR_CODE = {
    STOPPED: 'app_stopped',
    IN_USE: 'app_in_use',
    STOP_FAILED: 'app_stop_failed',
} as const;

AppContext, ILifecycleHook, AuthenticatorEntry, Server, AppTokens (each token's value type) and the re-exported orkos IModule and eldin IContainer types:

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type {
    Agent,
    Engine,
    IClock,
    INotifier,
    IStore,
} from '@chamvi/core';
import type {
    Authenticator,
    HttpRoute,
    IShutdown,
} from '@chamvi/http';
import type { IScheduler } from '@chamvi/scheduler';
import type { DataSource } from 'typeorm';
import type { AppConfig } from '../config/types.ts';
import type { Env } from '../env/types.ts';
import type { ILogger } from '../logger/types.ts';
import type { ModelProviders } from '../models/types.ts';

/** The container and module contracts a module factory is written against (eldin, orkos). */
export type { IContainer } from 'eldin';
export type { IModule } from 'orkos';

export type AppMode = 'start' | 'job' | 'migrate';

export type AppContext = {
    mode: AppMode;
    dev: boolean;
    env: Env;
    /** Aborts with the app_stopped ChamviError once stop() is called. */
    signal: AbortSignal;
    /** Logs through the app logger; during stop() also counts as a stop failure. */
    report(error: unknown, context?: Record<string, unknown>): void;
    /** createApp's `models`. */
    models?: ModelProviders;
    agents: readonly Agent[];
};

export interface ILifecycleHook {
    readonly name: string;
    /** After `new Engine(...)`, before `engine.start()` (start and job). */
    engineCreated?(engine: Engine): void | Promise<void>;
    /** start only: after scheduler.start(), before serve(). */
    started?(): void | Promise<void>;
    /** start only: inside createShutdown's scheduler step, after scheduler.stop(). */
    stopping?(): void | Promise<void>;
    /** After the engine drained (start and job), before the store closes. */
    drained?(): void | Promise<void>;
}

export type LifecyclePhase = 'engineCreated' | 'started' | 'stopping' | 'drained';

export type AuthenticatorEntry = {
    name: string;
    order: number;
    authenticator: Authenticator;
};

/**
 * The part of the HTTP server (srvx, from routup's `serve()`) the app uses,
 * typed structurally so srvx's declarations stay out of the package's types.
 */
export type Server = {
    readonly url?: string;
    ready(): Promise<unknown>;
    close(closeActiveConnections?: boolean): Promise<void>;
};

export type AppTokens = {
    CONTEXT: AppContext;
    CONFIG: AppConfig;
    LOGGER: ILogger;
    CLOCK: IClock;
    STORE: IStore;
    DATA_SOURCE: DataSource;
    ENGINE: Engine;
    SCHEDULER: IScheduler;
    SHUTDOWN: IShutdown;
    SERVER: Server;
    HTTP_ROUTES: HttpRoute;
    AUTHENTICATORS: AuthenticatorEntry;
    NOTIFIERS: INotifier;
    LIFECYCLE_HOOKS: ILifecycleHook;
};
ts
function runHooks(container: IContainer, phase: LifecyclePhase, ...args: unknown[]): Promise<void>; // every LIFECYCLE_HOOKS entry, in order
function throwIfStopped(context: AppContext): void; // throws the app_stopped error once stop() was called

Testing modules ​

ts
// @chamvi/app/testing
function buildModuleHarness(options?: ModuleHarnessOptions): ModuleHarness;          // not set up yet
async function createModuleHarness(options?: ModuleHarnessOptions): Promise<ModuleHarness>; // set up

The real module lifecycle without start() and a port, over exactly options.modules (list the built-ins a module needs, e.g. configModule(), storeModule(), engineModule()) on a container holding AppToken.CONTEXT (and CLOCK with clock): setup() runs every setup, every onReady and the start steps like the app and tears down and rethrows when one fails, reported collects every context.report() call (a throwing lifecycle hook lands there), stop() aborts the context signal and tears down.

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { Agent, IClock } from '@chamvi/core';
import type { IContainer } from 'eldin';
import type { Application, IModule } from 'orkos';
import type { Env } from '../env/types.ts';
import type { AppContext, AppMode } from '../module/types.ts';

export type ModuleHarnessReport = { error: unknown; context?: Record<string, unknown> };

export type ModuleHarnessOptions = {
    mode?: AppMode;
    dev?: boolean;
    env?: Env;
    modules?: IModule[];
    agents?: Agent[];
    models?: AppContext['models'];
    /** Registered as `AppToken.CLOCK` before setup (a config module may replace it). */
    clock?: IClock;
};

export type ModuleHarness = {
    application: Application;
    container: IContainer;
    context: AppContext;
    controller: AbortController;
    /** Everything `context.report()` received, in order. */
    reported: ModuleHarnessReport[];
    /** Runs `application.setup()` and the start steps, like the app; tears down and rethrows when it fails. */
    setup(): Promise<void>;
    /** Aborts the context signal (app_stopped) and tears every module down. */
    stop(): Promise<void>;
};

Models ​

ts
function createModelRegistry(options: ModelRegistryOptions): Promise<ProviderRegistryProvider>;
function installDefaultProvider(registry: ProviderRegistryProvider): () => void;

const BUILTIN_PROVIDERS: readonly BuiltinProvider[]; // anthropic, openai, google, mistral, xai

createModelRegistry imports every installed built-in provider package with its key from env, adds models, and checks every string model of agents: Agent "<a>": model "<m>" needs the <id> provider; install <pkg>., ... needs an API key; set <VAR>. or ... names the unknown provider "<id>"; available: <ids>. (each an InvalidAgentDefinitionError). installDefaultProvider sets globalThis.AI_SDK_DEFAULT_PROVIDER only when none is set and returns the undo; the engine module does both at start and runJob. The global is process-wide: two apps in one process share the first one's registry until it stops, and a default provider set before start is left alone (no model strings are checked then).

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { Agent } from '@chamvi/core';
import type { createProviderRegistry } from 'ai';
import type { Env } from '../env/types.ts';

/** createApp's `models`: providers by id, as `createProviderRegistry()` takes them. */
export type ModelProviders = Parameters<typeof createProviderRegistry>[0];

/** A provider package `createModelRegistry()` discovers when it is installed. */
export type BuiltinProvider = {
    /** The registry id, the part of a model string before `:`. */
    id: string;
    /** The npm package. */
    package: string;
    /** The factory the package exports. */
    factory: string;
    /** The env variable holding the API key. */
    apiKey: string;
    /** The env variable holding a base URL, when the provider reads one. */
    baseUrl?: string;
};

export type ModelRegistryOptions = {
    agents: readonly Agent[];
    env: Env;
    models?: ModelProviders;
    /** Loads a provider package; default `(s) => import(s)`. */
    importer?: (specifier: string) => Promise<unknown>;
};

Logger ​

ts
function createLogger(options?: LoggerOptions): ILogger;

One JSON line per entry ({ level, time, msg, ...fields, error? }, the error as { name, code, message, stack } only, never its cause) on stderr, or through write. The engine's, the scheduler's and the HTTP app's onError all go through it.

ts
/*
 * Copyright (c) 2026.
 * Author Peter Placzek (tada5hi)
 * For the full copyright and license information,
 * view the LICENSE file that was distributed with this source code.
 */

import type { IClock } from '@chamvi/core';
import type { LogLevel } from '../config/types.ts';

export interface ILogger {
    error(message: string, error?: unknown, fields?: Record<string, unknown>): void;
    warn(message: string, fields?: Record<string, unknown>): void;
    info(message: string, fields?: Record<string, unknown>): void;
    debug(message: string, fields?: Record<string, unknown>): void;
}

export type LoggerOptions = {
    /** Entries below it are dropped; default `info`. */
    level?: LogLevel;
    /** The `time` of every entry; default `systemClock`. */
    clock?: IClock;
    /** One call per JSON line (without the newline); default stderr. */
    write?: (line: string) => void;
};

export type SerializedError = {
    name?: string;
    code?: string;
    message: string;
    stack?: string;
};