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. Tools didn't fix this, because a tool answers a question and forgets, it has no memory between calls. What the agent needs is somewhere to keep what it's been told.

That's what defineState is: a durable, per-session slot the agent can write to and read back, turns later. You'll declare that slot 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) hands you a typed handle with two methods: get() reads the current value (running initial() the first time), and update(fn) replaces it. The value is durable, it survives across the step and turn boundaries of a session, so what you write on turn one is still there on turn five. We declared the handle at module scope so any tool that imports it shares 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 just 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 pulled the commuter straight from the garage. That's defineState doing its one job: holding onto something between turns.

Why it persisted

State checkpoints at step boundaries, the same durability you saw as session.waiting in 1.3. It's not a variable living in memory that a restart would wipe; it's written into the durable session. Redeploy mid-conversation and the commuter's still on file.

If the second turn asks "which bike?" anyway, check that both tools import the same handle from ../lib/garage.js, two separate defineState calls with the same name are still two slots in your code. And if recall_bikes never fires, nudge the persona: it should check 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 a memory. Next we give it judgment, a way to behave differently depending on who's standing at the counter.

Was this helpful?

supported.