Skip to content

Store Contract ​

Everything the engine persists goes through one interface, IStore. It is a set of small interfaces, so a store can be built and tested part by part.

InterfaceHoldsKey operations
IRunStoreRunscreateRun (insert-if-absent), getRun, updateRun with expectStatus/expectOwner guards, listRuns, claimRun/renewClaim leases
IStepStoreAppend-only steps per runappendStep, listSteps
ICheckpointStoreThe latest checkpoint per runsaveCheckpoint, latestCheckpoint
IIdempotencyStoreIntake keysclaimKey(scope, key, runId, ttlMs): the first caller wins
ILockStoreKeyed leasesacquire(key, owner, ttlMs), release(key, owner)
IEffectStoreRecorded effect resultsgetEffect, putEffect
IStateStorePer-agent statekvGet/kvSet, docList/docUpsert/docDelete
IArtifactStoreFiles of a run (attachments, files tools stored)putArtifact(artifact, data) (insert-if-absent), getArtifact (metadata), readArtifact (bytes), listArtifacts(runId)

IStore extends all eight.

Rules every store keeps ​

  1. One expiry rule. A claim, key or lock created at time T with ttlMs is expired at exactly T + ttlMs and can be taken over at that instant.
  2. Time comes from the clock. Expiry decisions use an injected IClock, never the database's clock, so tests can drive time and replicas agree on one rule.
  3. No aliasing. Values are copied in and out. Mutating an object after passing it to the store, or one the store returned, never changes what the store holds.
  4. Atomic means atomic across callers. createRun, claimKey, claimRun, renewClaim, acquire and putArtifact must hold under concurrency, also across processes.
  5. Ordering. listRuns orders by createdAt ascending, ties broken by id ascending; total ignores limit and offset. listArtifacts orders the same way.
  6. Guards throw. updateRun throws InvalidRunStateError when expectStatus does not match, or when expectOwner is given and the claim is absent or held by another owner (expiry is not checked there: a lease is lost once taken over). It throws RunNotFoundError for an unknown run.
  7. Checkpoints. saveCheckpoint keeps the highest stepIndex; an equal index replaces.
  8. State filters. docList where matches top-level fields by equality.
  9. Artifacts. putArtifact resolves false and keeps the stored artifact when the id exists. Bytes are copied in and out like every other value, and readArtifact returns a plain Uint8Array (no Buffer or other subclass). getArtifact and listArtifacts return metadata only.

The interfaces ​

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 { Artifact } from '../artifact/types.ts';
import type { 
    Checkpoint, 
    Run, 
    RunStatusLiteral, 
    Step, 
} from '../run/types.ts';

/**
 * Expiry rule shared by every TTL (run claims, idempotency keys, locks):
 * a lease/key created at time T with ttlMs expires AT T + ttlMs, i.e. at exactly
 * `expiresAt` it already counts as expired and can be taken over.
 *
 * Implementations must return copies: mutating a value passed in after the call,
 * or a value returned from the store, must never change what the store holds.
 */
export interface IRunStore {
    /**
     * Atomic insert-if-absent: resolves false and keeps the stored run when a run
     * with that id exists (two dispatches may create the run an idempotency key names).
     */
    createRun(run: Run): Promise<boolean>;
    getRun(id: string): Promise<Run | undefined>;
    /**
     * Throws InvalidRunStateError when `expectStatus` does not match the current
     * status, or when `expectOwner` is given and the run's claim is absent or held
     * by another owner (expiry is not checked: a lease is lost once taken over).
     */
    updateRun(id: string, patch: Partial<Run>, opts?: { expectStatus?: RunStatusLiteral[]; expectOwner?: string }): Promise<Run>;
    /**
     * Ordered by createdAt ascending, ties broken by id ascending. `total`
     * ignores limit/offset. Without `limit`, every matching run (from `offset`)
     * is returned: there is no default page size.
     */
    listRuns(q: {
        agent?: string;
        status?: RunStatusLiteral[];
        concurrencyKey?: string;
        limit?: number;
        offset?: number;
    }): Promise<{ data: Run[]; total: number }>;
    /** Atomic: only succeeds when unleased or the lease expired. */
    claimRun(id: string, owner: string, ttlMs: number): Promise<boolean>;
    /** Extends the lease; only succeeds for the current, unexpired owner. */
    renewClaim(id: string, owner: string, ttlMs: number): Promise<boolean>;
}

export interface IStepStore {
    appendStep(step: Step): Promise<void>;
    listSteps(runId: string): Promise<Step[]>;
}

export interface ICheckpointStore {
    /** Keeps the highest stepIndex; an equal stepIndex replaces (approval responses are appended that way). */
    saveCheckpoint(cp: Checkpoint): Promise<void>;
    latestCheckpoint(runId: string): Promise<Checkpoint | undefined>;
}

export interface IIdempotencyStore {
    /** Atomic: the first caller per (scope, key) creates; others get the winner's runId. */
    claimKey(scope: string, key: string, runId: string, ttlMs: number): Promise<{ created: boolean; runId: string }>;
}

export interface ILockStore {
    acquire(key: string, owner: string, ttlMs: number): Promise<boolean>;
    release(key: string, owner: string): Promise<void>;
}

export interface IEffectStore {
    getEffect(key: string): Promise<{ result: unknown } | undefined>;
    putEffect(key: string, result: unknown): Promise<void>;
}

export interface IStateStore {
    kvGet(agent: string, key: string): Promise<unknown>;
    kvSet(agent: string, key: string, value: unknown): Promise<void>;
    /** `where` matches by top-level equality. */
    docList(
        agent: string,
        coll: string,
        q?: { where?: Record<string, unknown>; limit?: number },
    ): Promise<Array<{ id: string; doc: unknown }>>;
    docUpsert(agent: string, coll: string, id: string, doc: unknown): Promise<void>;
    docDelete(agent: string, coll: string, id: string): Promise<void>;
}

/** Files of runs (attachments and tool outputs): metadata and bytes, both copied in and out. */
export interface IArtifactStore {
    /**
     * Atomic insert-if-absent: resolves false and keeps the stored artifact
     * (metadata and bytes) when an artifact with that id exists.
     */
    putArtifact(artifact: Artifact, data: Uint8Array): Promise<boolean>;
    /** Metadata only. */
    getArtifact(id: string): Promise<Artifact | undefined>;
    /** The bytes, as a plain Uint8Array (no Buffer or other subclass). */
    readArtifact(id: string): Promise<Uint8Array | undefined>;
    /** Metadata of a run's artifacts, ordered by createdAt ascending, ties broken by id ascending. */
    listArtifacts(runId: string): Promise<Artifact[]>;
}

export interface IStore extends IRunStore, IStepStore, ICheckpointStore, IIdempotencyStore, ILockStore, IEffectStore, IStateStore, IArtifactStore {}

Stores ​

StorePackageStatus
MemoryStore@chamvi/coreShipped. Development and tests
TypeORM@chamvi/store-typeormShipped. PostgreSQL, production

Redis is never required. Any store that passes the contract suite from @chamvi/test-kit works with the engine.