@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.
npm install @chamvi/app ai typeorm pgai, 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.
| Entry | Exports | Needs |
|---|---|---|
@chamvi/app | createApp, 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/authup | authupModule, oidcModule | @authup/server-adapter-kit, @authup/core-http-kit |
@chamvi/app/testing | buildModuleHarness, createModuleHarness |
createApp
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.
/*
* 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>;
}| Method | Builds | Rejects with |
|---|---|---|
start | config, store (migrated unless CHAMVI_MIGRATE=false), engine, scheduler (unless CHAMVI_SCHEDULER=false), http; starts the engine and scheduler once every module built, resolves once listening | the 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() |
stop | the shutdown sequence (see Start and stop); memoized, waits for a setup in flight | app_stop_failed when a teardown failed (each failure is logged) |
runJob | config, store (migrated), engine (not started: @chamvi/scheduler runJob starts, ticks and drains it); no scheduler, no http | the job's error; app_in_use |
migrate | config, store (migrateStore), store teardown | InvalidAgentDefinitionError 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:
| Code | When |
|---|---|
app_stopped | The lifecycle's abort reason once stop() was called; a start stopped while setting up rejects with it |
app_in_use | A second start, runJob or migrate on the same app |
app_stop_failed | stop() (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.
| Variable | Module | Type | Default | Required when |
|---|---|---|---|---|
PORT | http | int 1..65535 | 3000 | never |
HOST | http | string | 0.0.0.0 | never |
CHAMVI_BASE_PATH | http | empty or ^(?:\/[a-z0-9._~-]+)+$ | '' (health at /healthz, UI at /ui) | never |
CHAMVI_PUBLIC_URL | http, ui, notifiers | absolute URL | unset | an approval link is wanted |
CHAMVI_LOG_LEVEL | config | error, warn, info, debug | info | never |
DATABASE_URL | store | postgres://... | unset: MemoryStore (warned outside dev) | migrate; any multi-replica or CronJob deployment |
CHAMVI_MIGRATE | store | bool | true | never |
CHAMVI_PURGE_INTERVAL_MS | store | int, 0 disables | 3600000 (only with TypeormStore, on engine.clock) | never |
RESUME_SECRET | engine | string, at least 32 characters | none (dev: random per process, warned) | start, run-job |
CHAMVI_MAX_CONCURRENT_RUNS | engine | int >= 1 | 4 | never |
CHAMVI_SCHEDULER | scheduler | bool | true; false runs no in-process cron (CronJob mode) | never |
CHAMVI_READINESS_DELAY_MS | http shutdown | int | 0 | never |
CHAMVI_DRAIN_TIMEOUT_MS | http shutdown | int | 30000 (dev 5000) | never |
CHAMVI_CLOSE_TIMEOUT_MS | http shutdown | int | 5000 | never |
CHAMVI_FORCE_EXIT_MS | cli | int | 90000 (dev 15000) | never |
CHAMVI_TOKENS | static tokens | JSON [{ "value", "id", "permissions": [] }], value at least 16 characters and unique | unset (dev: one random * token, warned) | no other authenticator is configured, outside dev |
AUTHUP_URL, AUTHUP_CLIENT_ID, AUTHUP_CLIENT_SECRET | authupModule | URL, string, string | unset | authupModule() without options |
CHAMVI_UI_AUTHUP_CLIENT_ID, CHAMVI_UI_AUTHUP_REALM_ID | uiModule | string | unset (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_KEY | models | string | unset | a 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) | githubModule | string | none | githubModule() without options |
GITHUB_INSTALLATION_ID, GITHUB_APP_SLUG, GITHUB_API_URL | githubModule | int, slug, https URL | unset | never |
OPENCLAW_BASE_URL, OPENCLAW_HOOKS_TOKEN, OPENCLAW_CHANNEL, OPENCLAW_TO | openclawModule | URL, string, string, string | none | openclawModule() without options |
OPENCLAW_HOOKS_PATH, OPENCLAW_AGENT_ID, OPENCLAW_ACCOUNT_ID | openclawModule | string | /hooks, unset, unset | never |
CHAMVI_SANDBOX_PROVIDER | sandboxModule | local, docker, kubernetes | the provider option, else local | never |
CHAMVI_SANDBOX_NAMESPACE | sandboxModule | string | none | provider kubernetes |
CHAMVI_SANDBOX_IMAGE | sandboxModule | image ref | ghcr.io/chamvi/sandbox-node:24 | never |
function readConfig(env: Env, options: ReadConfigOptions): AppConfig;
function approvalUrlOf(config: AppConfig): ((ref: ApprovalRef) => string) | undefined; // needs CHAMVI_PUBLIC_URLThe env readers modules build on, each throwing InvalidAgentDefinitionError that names the variable:
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.
/*
* 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
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.
/*
* 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):
/*
* 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>;
}/*
* 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[];
};/*
* 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;
};/*
* 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;
};/*
* 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
// @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 formEach 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).
/*
* 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;
};/*
* 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'>;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;
};/*
* 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;
};/*
* 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;
};/*
* 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):
/*
* 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:
/*
* 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;
};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 calledTesting modules
// @chamvi/app/testing
function buildModuleHarness(options?: ModuleHarnessOptions): ModuleHarness; // not set up yet
async function createModuleHarness(options?: ModuleHarnessOptions): Promise<ModuleHarness>; // set upThe 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.
/*
* 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
function createModelRegistry(options: ModelRegistryOptions): Promise<ProviderRegistryProvider>;
function installDefaultProvider(registry: ProviderRegistryProvider): () => void;
const BUILTIN_PROVIDERS: readonly BuiltinProvider[]; // anthropic, openai, google, mistral, xaicreateModelRegistry 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).
/*
* 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
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.
/*
* 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;
};