Skip to content

@chamberu/core ​

The public API of @chamberu/core, taken from packages/core/src/index.ts. Contract interfaces carry an I prefix (IStore, IClock, INotifier, IAgentState); data shapes are unprefixed types.

ts
import { RunEngine, defineAgent } from '@chamberu/core';

Test helpers live in @chamberu/test-kit.

Agents ​

ts
function defineAgent<S extends StateSchema = StateSchema>(def: AgentDefinition<S>): Agent<S>;

const DEFAULT_MAX_STEPS: 20;
const DEFAULT_TIMEOUT_MS: 900_000; // 15 minutes

Throws InvalidAgentDefinitionError for an invalid name or duplicate trigger ids. See Agents.

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 { LanguageModel, ToolSet } from 'ai';
import type { Budget, RunContext } from '../run/types.ts';
import type { IAgentState, StateSchema } from '../state/types.ts';
import type { TriggerDefinition } from '../trigger/types.ts';

export type ToolFactoryContext<S extends StateSchema = StateSchema> = {
    state: IAgentState<S>;
    run: RunContext;
};

export type ApprovalPolicy = 'user' | ((input: unknown, ctx: { runtimeContext: RunContext }) => 'user' | 'allow' | 'deny');

export type AgentDefinition<S extends StateSchema = StateSchema> = {
    name: string;
    model: LanguageModel;
    instructions: string;
    triggers?: TriggerDefinition[];
    tools?: (ctx: ToolFactoryContext<S>) => ToolSet;
    /** Record<collection, StandardSchemaV1>. */
    state?: S;
    budget?: Partial<Budget>;
    /**
     * Tool name to policy (maps to AI SDK toolApproval). A call needing approval
     * pauses the run in `waiting`; one approval per pause: when a step asks for
     * several, the run waits on the first and the other calls are dropped from
     * the history, so the model requests them again after the resume.
     */
    approvals?: Record<string, ApprovalPolicy>;
    /** Default 24h; on timeout the call is denied. */
    approvalTimeoutMs?: number;
    /** Notifier name registered on the engine. */
    notify?: string;
};

export type Agent<S extends StateSchema = StateSchema> = {
    readonly name: string;
    readonly definition: Required<Pick<AgentDefinition<S>, 'budget'>> & AgentDefinition<S>;
};

Triggers ​

ts
function cron(expression: string, opts?: CronOptions): TriggerDefinition;
function http(opts?: HttpTriggerOptions): TriggerDefinition;
function manual(opts?: ManualTriggerOptions): TriggerDefinition;

TriggerSource, TriggerDefinition and TriggerEnvelope:

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 TriggerSource = 'cron' | 'http' | 'github' | 'mcp' | 'manual';

export type TriggerDefinition<P = unknown> = {
    source: TriggerSource;
    /** Unique within an agent, e.g. 'cron:*\/30 * * * *'. */
    id: string;
    /** Source-specific (cron: { expression, timezone?, overlap }). */
    options: Record<string, unknown>;
    // method signatures (bivariant): a TriggerDefinition<P> fits AgentDefinition.triggers
    concurrencyKey?(payload: P): string | undefined;
    idempotencyKey?(payload: P): string | undefined;
};

export type TriggerEnvelope<P = unknown> = {
    id: string;
    agent: string;
    trigger: string;
    source: TriggerSource;
    payload: P;
    idempotencyKey?: string;
    concurrencyKey?: string;
    thread?: string;
    receivedAt: Date;
};

CronOptions and OverlapPolicy, HttpTriggerOptions, ManualTriggerOptions:

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 OverlapPolicy = 'skip' | 'queue-one' | 'allow';

export type CronOptions = {
    timezone?: string;
    overlap?: OverlapPolicy;
    payload?: unknown;
};
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 { StandardSchemaV1 } from '@standard-schema/spec';

export type HttpTriggerOptions = {
    path?: string;
    schema?: StandardSchemaV1;
};
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 { StandardSchemaV1 } from '@standard-schema/spec';

export type ManualTriggerOptions = {
    schema?: StandardSchemaV1;
};

See Triggers & the Envelope.

RunEngine ​

ts
class RunEngine {
    constructor(opts: RunEngineOptions);

    dispatch(envelope: DispatchEnvelope): Promise<{ runId: string; deduplicated: boolean }>;
    getRun(id: string): Promise<Run | undefined>;
    on(event: RunEvent['type'], listener: (e: RunEvent) => void): () => void;

    start(): Promise<void>;
    stop(): void;
    drain(opts?: { timeoutMs?: number }): Promise<void>;
    idle(): Promise<void>;
    readonly isDraining: boolean;

    cancel(runId: string): Promise<void>;
    resume(token: string, decision: ResumeDecision): Promise<void>;
    tick(): Promise<void>;
    recover(): Promise<number>;
}
MethodDescriptionGuide
dispatchStores a queued run; deduplicates by idempotencyKeyTriggers
getRunReads a run from the storeRun Lifecycle
onSubscribes to run.updated or step.appended; returns an unsubscribe functionRun Lifecycle
startRecovers, starts polling and starts queued runsRecovery
stopStops polling onlyRecovery
drainWaits for in-flight runs (default 30 s), then marks the rest interruptedRecovery
idleResolves when nothing runs and nothing more can start
cancelCancels locally, in the store, or requests it from the owning replicaRecovery
resumeAnswers the approval a waiting run waits onApprovals
tickDenies expired approvals and re-sends undelivered notifications; runs every pollApprovals
recoverRequeues abandoned runs; runs on start and every pollRecovery
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 '../agent/types.ts';
import type { IClock } from '../clock/index.ts';
import type { INotifier } from '../notify/types.ts';
import type { Run, Step } from '../run/types.ts';
import type { IStore } from '../store/types.ts';
import type { TriggerEnvelope } from '../trigger/types.ts';

export type RunEvent = { type: 'run.updated'; run: Run } | { type: 'step.appended'; step: Step };

export type RunEngineOptions = {
    store: IStore;
    agents: Agent[];
    notifiers?: Record<string, INotifier>;
    /** Default `${hostname}-${pid}`. */
    owner?: string;
    /** Default 4. */
    maxConcurrentRuns?: number;
    /** Default 60_000, renewed every ttl/3 while running. */
    claimTtlMs?: number;
    /** HMAC secret for resume tokens; must not be empty. */
    resumeSecret: string;
    clock?: IClock;
    /** Receives errors that have no caller to throw to. Default console.error. */
    onError?: (error: unknown, run?: Run) => void;
};

export type DispatchEnvelope = Omit<TriggerEnvelope, 'id' | 'receivedAt'> & Partial<Pick<TriggerEnvelope, 'id' | 'receivedAt'>>;

export type ResumeDecision = {
    approved: boolean;
    reason?: string;
};
OptionDefault
notifiers{}
owner`${hostname}-${pid}`
maxConcurrentRuns4
claimTtlMs60_000, renewed and polled every claimTtlMs / 3
clocksystemClock
onErrorlogs to console.error

ApprovalRequest ({ approvalId, toolName, input }) is the tool call a waiting run waits on.

Runs ​

Run, RunStatusLiteral, RunContext, Step, Checkpoint, WaitPoint, Budget and Usage:

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 { ModelMessage } from 'ai';
import type { TriggerEnvelope } from '../trigger/types.ts';
import type { RunStatus } from './constants.ts';

export type RunStatusLiteral = `${RunStatus}`;

export type Usage = {
    inputTokens: number;
    outputTokens: number;
};

export type Budget = {
    maxSteps: number;
    maxTokens?: number;
    /**
     * Wall-clock limit per execution (not cumulative across resumes): each
     * execution of the run gets the full timeout again.
     */
    timeoutMs?: number;
};

export type RunContext = {
    runId: string;
    agent: string;
    trigger: TriggerEnvelope;
    signal: AbortSignal;
};

export type WaitPoint = {
    kind: 'approval';
    approvalId: string;
    toolName: string;
    input: unknown;
    expiresAt: Date;
    tokenId: string;
    /** Set once the agent's notifier accepted the approval request; tick() re-sends until then. */
    notifiedAt?: Date;
};

export type Run = {
    id: string;
    agent: string;
    status: RunStatusLiteral;
    trigger: TriggerEnvelope;
    concurrencyKey?: string;
    thread?: string;
    output?: unknown;
    error?: { code: string; message: string };
    usage: Usage;
    waiting?: WaitPoint;
    claim?: { owner: string; expiresAt: Date };
    /**
     * Set by cancel() on a replica that does not execute the run: the owner
     * aborts it on its next poll, and a later execution settles it cancelled.
     */
    cancelRequested?: boolean;
    createdAt: Date;
    updatedAt: Date;
    startedAt?: Date;
    finishedAt?: Date;
};

export type Step = {
    runId: string;
    index: number;
    kind: 'llm' | 'tool' | 'wait';
    name?: string;
    input?: unknown;
    output?: unknown;
    error?: unknown;
    startedAt: Date;
    finishedAt: Date;
};

export type Checkpoint = {
    runId: string;
    stepIndex: number;
    messages: ModelMessage[];
    /** AI SDK finish reason of the step this checkpoint was taken after. */
    finishReason?: string;
    createdAt: Date;
};

The RunStatus enum, whose values RunStatusLiteral is built from:

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 enum RunStatus {
    QUEUED = 'queued',
    RUNNING = 'running',
    WAITING = 'waiting',
    COMPLETED = 'completed',
    FAILED = 'failed',
    CANCELLED = 'cancelled',
    INTERRUPTED = 'interrupted',
}

Stores ​

ts
class MemoryStore implements IStore {
    constructor(opts?: { clock?: IClock });
}

IStore and its parts (IRunStore, IStepStore, ICheckpointStore, IIdempotencyStore, ILockStore, IEffectStore, IStateStore) are listed on the Store Contract page.

Effect tools ​

ts
function defineEffectTool<I, O>(def: Tool<I, O> & {
    effectKey?: (input: I, ctx: RunContext) => string;
    alreadyApplied?: (effectKey: string, input: I, ctx: RunContext) => Promise<O | undefined>;
}): EffectTool<I, O>;

function effectMarker(effectKey: string): string; // '<!-- chamberu:effect=<key> -->'

function wrapToolsForRun(tools: ToolSet, ctx: RunContext, store: IEffectStore): ToolSet;

const EFFECT: unique symbol; // marks a tool as an effect tool

See Effect Tools & Reconciliation.

Approvals ​

ts
function createResumeToken(secret: string, p: ResumeTokenClaims & { expiresAt: Date }): string;
function verifyResumeToken(secret: string, token: string, now: Date): ResumeTokenClaims;

type ResumeTokenClaims = { runId: string; approvalId: string; tokenId: string };

verifyResumeToken throws InvalidResumeTokenError for every failure. See Approvals & Resume Tokens.

State ​

ts
function createAgentState<S extends StateSchema = StateSchema>(
    agent: string,
    schema: S | undefined,
    store: IStateStore,
): IAgentState<S>;

function createStateTools<S extends StateSchema>(state: IAgentState<S>, schema: S): ToolSet;
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 { StandardSchemaV1 } from '@standard-schema/spec';

export type StateSchema = Record<string, StandardSchemaV1>;
export type Infer<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;

export interface IAgentState<S extends StateSchema = StateSchema> {
    get<T = unknown>(key: string): Promise<T | undefined>;
    set(key: string, value: unknown): Promise<void>;
    collection<K extends keyof S & string>(name: K): {
        list(q?: { where?: Partial<Infer<S[K]>>; limit?: number }): Promise<Array<{ id: string } & Infer<S[K]>>>;
        /** Validates `doc` via the collection's Standard Schema; throws ValidationError and stores nothing on failure. */
        upsert(id: string, doc: Infer<S[K]>): Promise<void>;
        remove(id: string): Promise<void>;
    };
}

See Agent State.

Notifiers ​

INotifier and Notification:

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 Notification = {
    kind: 'approval' | 'stuck' | 'result' | 'info';
    agent: string;
    runId: string;
    title: string;
    body: string;
    /** Signed token resuming the run (kind 'approval'). */
    resumeToken?: string;
    expiresAt?: Date;
};

export interface INotifier {
    readonly name: string;
    /**
     * Delivers a notification. `idempotencyKey` is stable per logical
     * notification (`runId:approvalId` for approvals), so transports can dedupe.
     */
    notify(n: Notification, opts: { idempotencyKey: string }): Promise<void>;
}

See Notifiers.

Clock ​

ts
const systemClock: IClock;
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 ClockTimerOptions = {
    /**
     * Keep the process alive until the timer fires (default false: engine
     * timers never hold the event loop open). Test clocks ignore it.
     */
    keepAlive?: boolean;
};

export interface IClock {
    now(): Date;
    setTimeout(fn: () => void, ms: number, opts?: ClockTimerOptions): { clear(): void };
}

systemClock timers never keep the process alive unless keepAlive is set; only the drain deadline sets it.

Errors ​

ChamberuError, InvalidAgentDefinitionError, InvalidResumeTokenError, InvalidRunStateError, RunNotFoundError, BudgetExceededError, TimeoutError, FatalToolError, LeaseLostError, ValidationError and ErrorCode:

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 const ErrorCode = {
    BUDGET_EXCEEDED: 'BudgetExceeded',
    FATAL_TOOL: 'FatalToolError',
    INVALID_AGENT_DEFINITION: 'invalid_agent_definition',
    INVALID_RESUME_TOKEN: 'invalid_resume_token',
    INVALID_RUN_STATE: 'invalid_run_state',
    LEASE_LOST: 'lease_lost',
    RUN_NOT_FOUND: 'run_not_found',
    TIMEOUT: 'Timeout',
    VALIDATION: 'validation_error',
} as const;

export type ErrorCode = typeof ErrorCode[keyof typeof ErrorCode];

See Errors for when each is thrown.