Vercel Logo

Remember the Bikes

A regular rolls up: "It's the commuter again, the rear shifting's gone sloppy." A good front-desk advisor doesn't say "and which bike is the commuter?" They already know it's the Surly Disc Trucker with the 700c wheels, because the shop keeps a card on file.

Our dispatcher has no card. Tell it about your Surly in one turn, ask it to book work on "the commuter" in the next, and it draws a blank. A tool can answer a question, but it has no memory between calls. The agent needs somewhere to keep what it has been told.

defineState provides a durable, per-session slot that the agent can write and read across turns. You'll declare it in agent/lib/garage.ts, then give the dispatcher two tools that use it.

Outcome

The dispatcher saves a customer's bike and recalls it in a later turn, without asking them to repeat the details.

Hands-on exercise

First, the memory itself. Create agent/lib/garage.ts:

agent/lib/garage.ts
import { defineState } from "eve/context";
 
export interface Bike {
  make: string;
  model: string;
  wheelSize?: string;
  notes?: string;
}
 
export interface Garage {
  readonly bikes: Readonly<Record<string, Bike>>;
}
 
export const garage = defineState<Garage>("bikeshop.garage", () => ({
  bikes: {},
}));

defineState(name, initial) returns a typed handle with two methods. get() reads the current value and runs initial() the first time. update(fn) replaces it. The value survives across the step and turn boundaries of a session, so data written on turn one is still there on turn five. Declaring the handle at module scope lets every importing tool share the same slot.

Now the hands. Two tools, both importing garage from ../lib/garage.js:

remember_bike takes a label (a short name like "the commuter") plus the bike's make, model, and optional wheelSize and notes. It writes the bike into the garage with garage.update(...), then returns the updated garage so the model can confirm.

recall_bikes takes no input and returns garage.get(), so the dispatcher can check what's on file before asking the customer to repeat themselves.

State is the agent's short-term memory, not a database

defineState lives and dies with the session: it's perfect for "what has this customer told me this conversation." Anything that must outlive the session, or be shared across customers, belongs in a real database. We're remembering bikes for the length of a chat, which is exactly its job.

Try It

Save a bike in one turn, then reference it in the next, two separate turns, same session:

you › it's a Surly Disc Trucker, 700c wheels. call it "the commuter".

  ⚙ remember_bike  { label: "the commuter", make: "Surly", model: "Disc Trucker", wheelSize: "700c" }

dispatcher › Got it, I've saved the commuter (Surly Disc Trucker, 700c) to your file.

you › the commuter's rear shifting feels sloppy. what do you recommend?

  ⚙ recall_bikes  {}
  ↳ { bikes: { "the commuter": { make: "Surly", model: "Disc Trucker", wheelSize: "700c" } } }

dispatcher › Sloppy rear shifting on the commuter usually means the derailleur
needs adjusting, sometimes a cable. A Basic Tune-Up ($65) covers that. Want me
to check openings?

The second turn never asked "which bike?" The dispatcher retrieved the commuter from the garage because defineState held the record between turns.

Why it persisted

State checkpoints at step boundaries, using the same durability you saw as session.waiting in 1.3. Eve writes the value into the durable session, so a restart or mid-conversation redeploy doesn't wipe the commuter from the file.

If the second turn asks "which bike?" anyway, check that both tools import the same handle from ../lib/garage.js. Two defineState calls with the same name are still separate slots in your code. If recall_bikes never fires, update the persona so it checks recall_bikes before asking a customer to repeat details.

Done-When

  • remember_bike writes a bike via garage.update(...) and returns the garage.
  • recall_bikes returns garage.get() with an empty input schema.
  • Saving a bike in one turn and referencing it in a later turn works without re-asking.
  • Both tools import the same garage handle from ../lib/garage.js.

Solution

agent/tools/remember_bike.ts
import { defineTool } from "eve/tools";
import { z } from "zod";
import { garage } from "../lib/garage.js";
 
export default defineTool({
  description:
    "Save a customer's bike so the shop remembers it across visits " +
    "(make, model, wheel size, and any standing notes).",
  inputSchema: z.object({
    label: z.string().describe("A short name for the bike, e.g. 'the commuter'."),
    make: z.string(),
    model: z.string(),
    wheelSize: z.string().optional(),
    notes: z.string().optional(),
  }),
  async execute({ label, make, model, wheelSize, notes }) {
    garage.update((g) => ({
      bikes: { ...g.bikes, [label]: { make, model, wheelSize, notes } },
    }));
    return garage.get();
  },
});
agent/tools/recall_bikes.ts
import { defineTool } from "eve/tools";
import { z } from "zod";
import { garage } from "../lib/garage.js";
 
export default defineTool({
  description: "Read the bikes the shop has on file for this customer.",
  inputSchema: z.object({}),
  async execute() {
    return garage.get();
  },
});

The dispatcher now has tools and memory. Next, it will choose a playbook based on who's standing at the counter.

Was this helpful?

supported.