Back to Copilotkit

Persistence

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

1.62.16.7 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.

<OpsPlatformCTA variant="inline" title="Join the waitlist for managed Slack and Teams agents" body="This page shows how to bring your own Redis or PostgreSQL store. Join the waitlist if you want CopilotKit Intelligence to manage durable state, runtime handoff, approvals, analytics, and release operations for Slack and Teams." ctaLabel="Join the waitlist" surface="docs_bots_persistence_managed_agents_waitlist" href="https://www.copilotkit.ai/opentag-managed" />

<Steps> <Step> ### Understand the default: MemoryStore
`createBot` uses `MemoryStore` by default — no configuration needed. It is built-in, zero-dependency, and good enough for local development and ephemeral environments. All data is lost when the process restarts.

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

const bot = createBot({
  adapters: [/* ... */],
  agent: (threadId) => /* ... */,
  // No store.adapter — MemoryStore is the default.
});
```

<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 a durable `StateStore` implementation 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> ### Add a durable store for production
For persistence across restarts and horizontal scale, implement the [`StateStore`](/reference/bot/types/StateStore) interface and pass your instance as `store.adapter`:

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

export class MyDurableStore implements StateStore {
  // Implement all five primitive groups: kv, list, lock, dedup, queue.
  // See the StateStore reference for the full contract and signatures.
}
```

```ts title="bot.ts"
import { createBot } from "@copilotkit/bot";
import { MyDurableStore } from "./my-store.js";

const bot = createBot({
  adapters: [/* ... */],
  agent: (threadId) => /* ... */,
  store: {
    adapter: new MyDurableStore(),
  },
});
```

The package ships `runStateStoreConformance(name, make, teardown?)` — a Vitest describe block 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:

```ts
import { runStateStoreConformance } from "@copilotkit/bot";

runStateStoreConformance(
  "MyDurableStore",
  () => new MyDurableStore(),
  async (s) => s.close(), // optional teardown
);
```

<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> <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: new MyDurableStore(),
  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: new MyDurableStore(),
  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 { 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: new MyDurableStore(), // add a durable adapter for persistence across restarts
    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> </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.