Skip to content

Self-hosted agents

Agents that keep the keys.

You write the agent with the AI SDK. chamberu queues its runs, checkpoints every step, pauses for approvals and resumes interrupted runs, in your own Node process and database.

agents/triage.tsrunning
ts
export const triage = defineAgent({
    name: 'triage',
    model,
    instructions: 'Label new issues and close duplicates.',
    triggers: [
        http({ path: '/issues' }),
        cron('0 * * * *'),
    ],
    tools: () => ({
        // recorded per effect key: retries never relabel
        label: defineEffectTool({
            description: 'Adds a label to an issue.',
            inputSchema: z.object({
                issue: z.number(),
                label: z.string(),
            }),
            execute: async (i) => addLabel(i.issue, i.label),
        }),
        close: tool({
            description: 'Closes an issue as a duplicate.',
            inputSchema: z.object({ issue: z.number() }),
            execute: async ({ issue }) => closeIssue(issue),
        }),
    }),
    // waits in `waiting` until the owner signs off
    approvals: { close: 'user' },
    notify: 'owner',
});
waitingclose(#412) waits for the owner, resume token signed

What the AI SDK leaves out

The layer between a model call and an agent you can trust.

  • 01

    One intake for every trigger

    A webhook, a cron tick or a manual call becomes the same envelope and the same queued run. Idempotency keys turn redeliveries into the existing run; concurrency keys keep two runs for one issue apart.

    dispatch({ idempotencyKey })

  • 02

    Runs survive restarts

    A checkpoint after every step. drain() hands unfinished runs back, and recover() continues them from the latest checkpoint on any replica, without applying a recorded effect twice.

    drain() → recover()

  • 03

    Ask before acting

    A tool call that needs a human pauses the run in waiting, with a signed resume token. Approve or deny it an hour or a day later, from any replica.

    resume(token, { approved })

How a run flows

Seven statuses, one store.

Every run lives in your store, from dispatch to its final status. Whatever stops it halfway, the next execution starts from the latest checkpoint.

Run lifecycle
  • queued
  • running
  • waiting
  • interrupted
  • completed
  • failed
  • cancelled
dispatch() stores a queued run. The engine claims it and runs it, with a checkpoint after every step, until it ends completed, failed or cancelled. A tool call that needs approval pauses the run in waiting with a signed resume token; resume() requeues it. drain() or a crash leaves a run interrupted; recover() requeues it, and the next execution continues from the latest checkpoint.

Packages

Small today, planned in the open.

The engine and its test helpers ship now. The scheduler, the HTTP API, the Postgres store and the rest are designed milestone by milestone.

  • @chamberu/store-typeormplanned M1Postgres store through TypeORM, passing the store contract suite.
  • @chamberu/schedulerplanned M1Fires cron() triggers once per tick across replicas.
  • @chamberu/httpplanned M1REST control API, server-sent events, webhook intake and an MCP endpoint.
  • @chamberu/githubplanned M1GitHub App auth, webhook triggers and issue tools with reconciliation.
  • @chamberu/sandbox-*planned M1Disposable workspaces, local for development and a pod per workspace in production.
  • @chamberu/uiplanned M1A dashboard with live steps, an approvals inbox, agent state and schedules.

Roadmap

Start with one agent.

Define an agent, dispatch a run and watch it settle. MemoryStore needs no database, so the quick start runs in a single file.

npm install @chamberu/core ai zod