Back to Copilotkit

Persistence

showcase/shell-docs/src/content/docs/bots/persistence.mdx

1.61.17.3 KB
Original Source

Where you're starting from: a dev bot where every restart wipes button handlers, resets thread state, and drops duplicate-detection memory. A user clicks a button five minutes after a deploy and nothing happens.

Where you're headed: the same bot, wired to a durable store, where actions survive restarts, concurrent turns are serialised with a turn lock, and each thread carries typed state that persists across runs.

<Steps> <Step> ### Choose a storage backend
`createBot` accepts a `store.adapter` — the backing store for actions, locks, dedup, and state. Pick the backend that matches your deployment:

<Tabs items={["Memory", "Redis", "PostgreSQL"]}>
  <Tab value="Memory">
    The default. No install needed. State is lost on every restart — useful for local development and ephemeral environments.

    ```ts title="bot.ts"
    import { createBot, MemoryStore } from "@copilotkit/bot";

    const bot = createBot({
      adapters: [/* ... */],
      agent: (threadId) => /* ... */,
      store: {
        adapter: new MemoryStore(), // default — can be omitted
      },
    });
    ```
  </Tab>
  <Tab value="Redis">
    Durable, fast, production-ready. Actions survive restarts; the store works across multiple bot instances pointing at the same Redis URL.

    ```bash
    npm install @copilotkit/bot-store-redis
    ```

    ```ts title="bot.ts"
    import { createBot } from "@copilotkit/bot";
    import { createRedisStore } from "@copilotkit/bot-store-redis";

    const bot = createBot({
      adapters: [/* ... */],
      agent: (threadId) => /* ... */,
      store: {
        adapter: createRedisStore({ url: process.env.REDIS_URL! }),
      },
    });
    ```
  </Tab>
  <Tab value="PostgreSQL">
    Relational persistence with automatic schema management. Set `autoMigrate: true` to let the store create its tables on first boot.

    ```bash
    npm install @copilotkit/bot-store-postgres
    ```

    ```ts title="bot.ts"
    import { createBot } from "@copilotkit/bot";
    import { createPostgresStore } from "@copilotkit/bot-store-postgres";

    const bot = createBot({
      adapters: [/* ... */],
      agent: (threadId) => /* ... */,
      store: {
        adapter: createPostgresStore({
          connectionString: process.env.DATABASE_URL!,
          autoMigrate: true,
        }),
      },
    });
    ```
  </Tab>
</Tabs>

<Callout type="info" title="Durable actions are automatic">
  No code changes needed for button handlers. With `MemoryStore`, a button clicked after a restart is acknowledged by Slack but ignored — the handler is gone. Swap in Redis or PostgreSQL and the same handler is fetched from the store and executed normally. The [ActionStore reference](/reference/bot/types/ActionStore) documents the contract if you want to inspect it.
</Callout>
</Step> <Step> ### Understand what you get: turn locks and dedup
A durable store does more than keep button handlers alive. The bot uses the store for two additional guarantees:

**Turn lock** — at most one agent run per thread at a time. If a second event arrives while the agent is still running, `onLockConflict` decides what happens:

```ts
store: {
  adapter: createRedisStore({ url: process.env.REDIS_URL! }),
  onLockConflict: "drop",   // silently ignore the new event (default)
  // onLockConflict: "force",  // evict the running turn and start fresh
  // onLockConflict: async ({ thread }) => { /* custom logic */ },
  lockTtl: 60_000,          // ms before a stale lock auto-expires (default: 60 000)
},
```

**Dedup** — identical inbound events (Slack sometimes delivers the same event twice) are deduplicated by their event id. The window is controlled by `dedupTtl`:

```ts
store: {
  adapter: createRedisStore({ url: process.env.REDIS_URL! }),
  dedupTtl: 300_000, // 5-minute dedup window (default: 300 000)
},
```

<Callout type="info" title="Lock and dedup TTLs">
  Both values are in milliseconds. The defaults are conservative — raise `dedupTtl` if your platform retries events after more than five minutes, or lower `lockTtl` if you want stale locks to expire faster during long agent runs.
</Callout>
</Step> <Step> ### Add typed per-thread state
Each conversation thread can carry structured data that persists between turns. Pass a [Standard Schema](https://standardschema.dev) (e.g. a Zod object) as `store.state` and the `Thread` class types `thread.state()` and `thread.setState()` to match it — validated at runtime on every write.

```ts title="bot.ts"
import { createBot } from "@copilotkit/bot";
import { createRedisStore } from "@copilotkit/bot-store-redis";
import { z } from "zod";

const TicketFlow = z.object({
  stage: z.enum(["triage", "assigned", "resolved"]).default("triage"),
  assignee: z.string().optional(),
  priority: z.enum(["low", "medium", "high"]).optional(),
});

const bot = createBot({
  adapters: [/* ... */],
  agent: (threadId) => /* ... */,
  store: {
    adapter: createRedisStore({ url: process.env.REDIS_URL! }),
    state: TicketFlow,
  },
});

bot.onMention(async ({ thread }) => {
  // thread.state() returns TicketFlow — typed and validated
  const current = await thread.state();

  if (current.stage === "triage") {
    await thread.setState({ stage: "assigned", assignee: "on-call" });
    await thread.runAgent({
      prompt: "The ticket has been assigned to on-call. Summarise next steps.",
    });
    return;
  }

  await thread.runAgent();
});
```

`thread.setState()` merges the partial patch into the stored state and validates the result against your schema — it throws if the merged object doesn't conform, so invalid state never reaches the store.

<Callout type="info" title="Thread API reference">
  Full signatures for `state()`, `setState()`, `runAgent()`, and the rest of the thread surface are in the [Thread reference](/reference/bot/classes/Thread).
</Callout>
</Step> <Step> ### Bring your own store
If neither Redis nor PostgreSQL fits your infrastructure, implement the `StateStore` interface directly:

```ts title="my-store.ts"
import type { StateStore } from "@copilotkit/bot";

export class MyCustomStore implements StateStore {
  // Required methods: get, set, delete, lock, unlock, dedup
  // See the StateStore reference for the full contract.
}
```

The package ships `runStateStoreConformance(store)` — a test suite that verifies your implementation against the full contract (locking, dedup, state round-trips). Run it in your test suite before wiring the store into production.

<Callout type="info" title="StateStore reference">
  The full interface definition and conformance test helper are documented in the [StateStore reference](/reference/bot/types/StateStore).
</Callout>
</Step> </Steps>

You've reached point B. The bot now has a durable store: button handlers survive restarts, concurrent turns are serialised, duplicates are dropped, and every thread carries typed state that outlives the process.